Appearance
course-service Agent Tasks
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: per-service
Dispatch ID: SVC-007
Owns: services/course-service.
Must read:
docs/agents/handoff-phase-8-course-foundation.md- legacy course, enrollment, progress, and public catalog modules:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/courses/courses.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/courses/courses.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/courses/courses.spec.ts.
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/courses/**- course/progress Prisma models in
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/prisma/schema.prisma.
Writable files:
services/course-service/**contracts/openapi/services/course-service.yamldocs/api/course-service.md
Orchestrator-owned files:
- billing/wallet paid-access decisions, file/question/exam shared contracts, gateway route cutovers, and shared deploy manifests unless delegated.
Delegated gateway verification scope:
- Agents may maintain only non-default student course rehearsals in
deploy/gateway/routes.student-courses-read-native-example.json,deploy/gateway/routes.student-courses-read-native-localhost-example.json,deploy/gateway/routes.student-course-progress-native-example.json, anddeploy/gateway/routes.student-course-progress-native-localhost-example.json; default public course routes remain orchestrator-owned. - Route proof must use
scripts/test/student-course-route-coverage.shandscripts/test/student-course-progress-route-coverage.sh, with QA evidence underdocs/qa/student-course-*.md. - Makefile targets:
test-student-course-routes,test-student-course-progress-routes, andtest-student-course-browser.
Initial tasks:
- [x] Add workflow tests for course authoring -> sections -> lessons -> enrollment -> progress.
- [x] Audit material references so file bytes remain in file/document-service.
- Done: define and persist management-only quiz/question references.
GET /v1/courses/integration-contracts/question-examnames question-bank-service and exam-service owner APIs; native/v1routes keep only ids, placement metadata, and optional copied titles in course-service. - [x] Add usecase-level tests for enrollment, archive, public catalog, and progress behavior; HTTP-only coverage is too thin for public promotion.
- The cross-service mastery/recommendation/report task is
analytics-course-learning-projection.md. It requires Course catalog and progress snapshots plus a trusted course-attempt context; Course must not derive mastery or map an attempt byexamIdalone. File-streaming, owner API validation, and public/Gateway question/quiz delivery remain separate from the local reference slice.
Current hardening:
services/course-service/internal/usecase/course_test.gocovers manager guards, section unlinking, lesson-owned material/progress cleanup, enrollment expiry, video progress auto-completion, material view counting, and public preview filtering.- Student/public material projections now respect the visibility of the attached lesson; standalone course materials continue to use their own published/preview flags.
- Done: add
SVC-007-student-course-route-proofHTTP coverage for enrolled student list/detail/progress routes, published lesson/material projection, cross-organization denial, and non-student denial. Static route guards and the browser self-test remain non-default evidence; live/browser promotion proof is still required. - Student-web's opt-in native course detail now applies a successful lesson progress write locally instead of serially reloading the same detail. The owner-service refresh remains the next route load; unit and browser-intercept regressions require no additional detail read after the write. This is a frontend latency improvement only and does not change the default route.
- Course search projection now emits copied
COURSEdocuments atGET /v1/courses/search-projectionsfor search-service rebuilds. Keep search indexing as copied data owned by search-service, never a direct cross-service DB read. 000003_course_search_projection_outbox.sqlnow adds a Course-owned durable projection revision/outbox and opt-in JetStream publisher. Parent course writes, count-affecting lesson/material/enrollment/progress mutations, and an organization move commit the copied event with the owner transaction. Archive is anUPSERTwithARCHIVEDbecause the owner rebuild route includes archived courses; only a physical course delete emitsDELETE. Raw lesson/material content, video/view changes, student ids, file URLs, and storage keys are not copied. The publisher is disabled unlessCOURSE_SEARCH_PROJECTION_EVENT_TRANSPORT=nats(or an equivalent supported NATS selector) is configured with a Postgres store.000004_course_question_exam_references.sqladds local lesson-question and quiz reference rows with no foreign keys to owner-service databases. Duplicate(lesson,question)and(course,exam)saves update local placement metadata. Lesson delete cascades question refs and unbinds quiz lesson placement; section delete unbinds quiz section placement. Course read/search projection count fields are updated, but external ids and copied titles are not emitted.000005_course_learning_catalog_events.sqladds an independent transactional Course catalog event feed for Analytics. It must never reuse the Search outbox/NATS subject.GET /v1/courses/learning-catalog-eventsis anADMIN-only direct owner API with organization/cursor scoping and no Gateway route. Each payload has a stablesourceEventId, monotonic catalog revision,UPSERTorDELETEintent, only published topic-bound lesson placements and their lesson-bound quiz references. It excludes raw lesson content, questions/answers, media URLs, profile fields, and ambiguous topicless or course-level quiz placements.000006_course_learning_catalog_outbox.sqladds a separate Course-owned delivery outbox for the immutable000005snapshots. When explicitly enabled withCOURSE_LEARNING_ANALYTICS_DELIVERY_ENABLED=trueand an Analytics URL, its worker lease-claims, retries, and dead-letters delivery to the normal Analytics event-ingestion contract. It is disabled by default, never blocks the Course mutation that wrote the source event, never lets Analytics query Course storage, and does not materialize mastery rows or add a Gateway route.000007_course_learning_progress_outbox.sqlmakes organization-scopedcourse.lesson_progress.saved.v1delivery durable too. Progress writes never call Analytics on the learner request path: the Course transaction records a bounded, replayable payload, while the same opt-in worker lease-claims, retries, and dead-letters the later delivery. It must remain separate from the catalog snapshot feed and must not add a default Gateway route.COURSE_LEARNING_ANALYTICS_RUNTIME_CONFIRM=local-postgres make test-course-learning-analytics-runtimeis the owner-to-owner runtime proof: fresh Course and Analytics databases plus their native binaries create a published topic-bound lesson, wait for catalog delivery, record a student's completion, and read the resulting mastery through Analytics HTTP. It proves the opt-in local transport/materialization slice only; it does not promote a Gateway route, reuse a configured database, or establish deployed evidence.- Course now resolves a private quiz-attempt context at
POST /v1/internal/courses/{courseId}/quiz-links/{referenceId}/attempt-contextfor the authenticated Gateway adapter only. It validates student active enrollment, published Course and lesson, nonblank lesson topic, local quiz reference, and matching Exam id without querying Exam or Attempt databases. The response is bounded ids plus Course validation metadata; a browser may identify the selected placement but cannot assert this context. The adapter binds it to Attempt's v2 producer contract; it is not a public/default route. - Course enrollment grants now emit optional best-effort analytics-service producer events when
ANALYTICS_SERVICE_URLorANALYTICS_SERVICE_BASE_URLis configured. The event iscourse.enrollment.granted.v1withsourceService=course-service,sourceEventId=course_enrollment:{enrollmentId}:granted, and a bounded enrollment payload. Analytics outages must not fail enrollment writes. - Course video progress and material views now also emit optional best-effort analytics-service producer events. The events are
course.video_progress.saved.v1andcourse.material_view.saved.v1with timestamped source ids for mutable rows. Payloads include progress counters and display metadata only; no lesson content, media URLs, storage keys, file URLs, or profile PII. Remaining analytics work: durable rebuild/backfill policy and broader mastery projections.
Acceptance:
- Course owns course/progress state only.
- No wallet purchase or question source-of-truth logic is added here.
- Public course projections are covered by route/gateway tests before cutover.
- Purchase and paid access stay legacy or future billing/wallet integrations; no local wallet logic is added to course-service.
- Analytics producer events are replay-safe source metadata only; course-service must not read analytics-service databases or own analytics projections.
- Search projection outbox payloads remain redacted copied-index data; do not add raw course lesson/material content, student identifiers, file URLs, or storage keys to the event.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/course-service/...GOTOOLCHAIN=go1.25.11 go test ./services/course-service/internal/usecase -run TestQuestionExamIntegrationContractPreservesOwnerBoundaries -count=1 -vGOTOOLCHAIN=go1.25.11 go test ./services/course-service/internal/http -run TestQuestionExamIntegrationContractRoute -count=1 -vGOTOOLCHAIN=go1.25.11 go test ./services/course-service/internal/usecase -run TestCourseSearchProjectionListsRebuildDocuments -count=1 -vGOTOOLCHAIN=go1.25.11 go test ./services/course-service/internal/http -run TestCourseSearchProjectionBackfillRoute -count=1 -vCOURSE_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-course-search-projection-outbox-postgresCOURSE_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-course-learning-catalog-events-postgresCOURSE_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-course-learning-catalog-delivery-postgresCOURSE_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-course-learning-progress-delivery-postgresGOTOOLCHAIN=go1.25.11 go test ./services/course-service/internal/usecase -run 'TestCourseReferences|TestCourseReferenceValidation' -count=1 -vGOTOOLCHAIN=go1.25.11 go test ./services/course-service/internal/http -run TestCourseReferenceRoutes -count=1 -vCOURSE_SEARCH_PROJECTION_BROKER_SMOKE_CONFIRM=disposable-runtime make test-course-search-projection-broker-livemake test-student-course-routes test-student-course-progress-routesSTUDENT_COURSE_BROWSER_SELF_TEST=1 make test-student-course-browser; live mode requires a student token and must observe frontend/student/coursestraffic through the gateway before public route promotion.docs/qa/student-course-route-rehearsals.mdrecords the current proof gate: static route-table coverage is not public promotion evidence until a later live/browser smoke captures real student frontend traffic through the gateway.