Appearance
Operational Producer Events Task Pack
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: orchestrator
Owner: orchestrator with the producing service owner and the consuming service owner. Do not treat this pack as a standalone deployable service.
Purpose: finish versioned, replay-safe producer event contracts between owner services and the operational services that store copied views or delivery state: analytics-service, notification-service, and audit-service.
Must read:
docs/architecture/service-responsibility-matrix.mddocs/agents/service-workflow-test-matrix.md- producing service task pack
- consuming service task pack
- consuming service OpenAPI contract
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/attempts/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/analytics/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/notifications/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/admin/**
Writable files:
- The selected producer service files only, for example
services/attempt-service/**for an attempt analytics producer. - The selected producer service API docs/OpenAPI mirrors only if the public or native service contract changes.
- The selected producer service task pack.
- The selected consumer task pack only for status/handoff wording.
Orchestrator-owned files:
- Shared route tables, root
Makefile, Compose, Helm, VitePress navigation, this pack, and service wave board updates unless explicitly delegated.
Event contract invariants:
- Producers must call the consumer service API; they must never read or write a consumer service database directly.
- Every replayable event must carry
sourceService, stablesourceEventId, event type, entity, entity id, occurred timestamp, organization scope when available, and redacted payload metadata. - Producer delivery must be best-effort or outbox-backed so the producer's canonical write path is not made unavailable by an operational projection service outage.
- Consumer idempotency must be by
sourceService+sourceEventId. - Public
/api/*promotion is out of scope unless a dedicated gateway task adds route coverage, browser/runtime proof, and rollback evidence.
Current priority slices:
- Done: Attempt -> Analytics publishes
attempt.submitted.v1andattempt.graded.v1lifecycle/result metadata to analytics-service event ingestion with replay-safe source ids. - Done: Course -> Analytics publishes
course.enrollment.granted.v1metadata on successful enrollment grants with replay-safe source ids. - Done: Course -> Analytics publishes
course.lesson_progress.saved.v1metadata fromRecordLessonProgress, with mutable progress source ids that include the savedlastViewedAttimestamp. - Done: Course -> Analytics publishes
course.video_progress.saved.v1andcourse.material_view.saved.v1with timestamped source ids and redacted display metadata only. - Done: Exam -> Analytics publishes
exam.published.v1metadata after native publish succeeds, with replay-safe{examId}:publishedsource ids and redacted access-link metadata. - Done: Exam -> Analytics publishes
exam.assigned.v1metadata after native published-exam classroom assignment upserts, with replay-safe{assignmentId}:assignedsource ids and redacted exam/assignment refs. - Done: Exam -> Analytics publishes
exam.results_released.v1metadata after native published/closed exam result release, with stableexam:{examId}:results-releasedsource ids and redacted exam visibility metadata only. - Done: Exam -> Notification publishes resolved-recipient
exam.assigned.notification.v1events after native published-exam assignment when the internal assignment request supplies student/parent recipients. Source ids are{assignmentId}:notification:exam_assignedand{assignmentId}:notification:exam_assigned_parent; notification-service owns replay/preference handling, while exam-service does not discover recipients. - Done: Classroom -> Notification publishes
classroom.notification.sent.v1events from internalPOST /v1/classrooms/{classroomId}/notifications. Source ids areclassroom:{classroomId}:notification:{clientRequestId}and student recipients come from classroom-service ownedSTUDENTmember rows only. When profile-service is configured, classroom-service calls/v1/internal/profiles/parent-recipients/resolvewith those student ids and publishes a separateclassroom.notification.parent.sent.v1event with source idclassroom:{classroomId}:notification:{clientRequestId}:parents. - Done: Exam -> Attempt result-policy propagation sends
exam.results_released.v1through authenticated internalPOST /v1/events/exam-results-released, updating copiedexam_results_released_atsnapshots byexamIdwithout cross-service database access or public route promotion. The direct handoff is best-effort and replay-safe; durable outbox delivery remains a later hardening slice. - Done: Classroom -> Analytics publishes
classroom.membership.snapshot.v1after native membership mutations by reloading classroom + member refs and excluding profile PII. Source ids useclassroom:{classroomId}:membership-snapshot:{occurredAtRFC3339Nano}. - Done: Admin -> Notification publishes explicit-recipient
admin.notification.broadcast.v1events from internalPOST /v1/admin/notification-broadcasts. Source ids areadmin:broadcast:{clientRequestId}and recipients must be supplied by the caller; admin-service does not resolve legacyALL/ROLEStargets. - Support -> Notification: publish notification intent events only with already-resolved recipients or with an approved resolver handoff. Notification-service must not discover recipients by cross-service database joins. Notification-service is consumer-ready for
POST /v1/events/notificationandPOST /v1/events/weak-topic-alert; the first native support producer is now available at admin-servicePOST /v1/support/notification-eventsfor callers that already have concrete recipients. It emitssupport.ticket.notification.v1withsourceEventId=support:ticket:{ticketId}:notification:{clientRequestId}and preserves legacy inbox types such asSUPPORT_TICKET_CREATED,SUPPORT_TICKET_REPLY, andSUPPORT_TICKET_STATUS. Recipient discovery, support ticket persistence, public/api/support*route cutover, and runtime proof remain separate work. - IAM/Admin/Gateway -> Audit: publish redacted security/admin/route operation events to audit-service with replay-safe source ids and no secrets. IAM, admin, payment, billing, wallet, notification, and profile have first best-effort audit projection slices; gateway now has a first best-effort route-operation projection using
sourceService=api-gateway,sourceEventId=gateway:route:<requestId>,action=gateway.route.request, and redacted query-key-only metadata. Richer organization-membership audit semantics, outbox-backed retry/replay, runtime smoke, and rollback evidence remain separate work.
Acceptance:
- The selected producer has focused tests proving payload shape and replay id.
- The selected producer has a test for disabled or upstream-failure behavior.
- The selected consumer task pack records whether the handoff is done or still pending.
- OpenAPI service and contract mirrors remain byte-identical if changed.
- Default public route behavior is unchanged.
Verification:
- Producer service package tests, for example
GOTOOLCHAIN=go1.25.11 go test ./services/attempt-service/... -count=1 - Consumer service package tests when the consumer contract changed.
- OpenAPI YAML parse and mirror diff for changed service contracts.
make test-service-task-packspnpm docs:buildgit diff --check
Closeout:
- Name the producer and consumer services.
- List the event type, source id format, and target endpoint.
- List failure behavior: disabled, best-effort failure, retry, or outbox.
- State that no public route was promoted unless a gateway task proves it.