Appearance
notification-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-020
Current implementation: services/notification-service.
Owns: notifications, templates, preferences, read/delete state, parent-facing alert rows, notification events, delivery-state metadata, and provider delivery orchestration/attempts for already-resolved recipients. IAM owns OTP issuance and verification; notification-service only delivers an already-authorized OTP message through a provider adapter.
Producer collaboration: services/attempt-service may emit the bounded grade-release event contract, but it is an event producer dependency rather than an implementation owner of this SVC-020 packet. Any producer change keeps the services/attempt-service lock and focused attempt tests in addition to the notification verification below.
Must read:
docs/agents/handoff-phase-9-notification-foundation.md- legacy notification and parent alert modules:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/notifications/notification.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/notifications/notifications.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/app-data.notifications.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/analytics/analytics.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/admin/inbox.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/support/support.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/app-data.classrooms-engagement.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/app-data.exam-runtime-core.ts.
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/notifications/**- parent alert routes in
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/analytics/**. - admin broadcast/support fanout in
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/admin/**and/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/support/**. - classroom/exam notification producers in
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/**.
Writable files:
services/notification-service/**- For bounded
SVC-020-attempt-grade-notification-producer-contractwork only:services/attempt-service/**,services/attempt-service/api/openapi.yaml,contracts/openapi/services/attempt-service.yaml, anddocs/api/attempt-service.md. contracts/openapi/services/notification-service.yamldocs/api/notification-service.md
Orchestrator-owned files:
- profile/classroom parent-student resolution, delivery provider secrets, gateway route cutovers, and shared deploy manifests unless delegated.
Delegated gateway verification scope:
- Only when the orchestrator assigns a notification route/live/browser proof subtask, this service agent may update non-default
deploy/gateway/routes.notifications-*.json,deploy/gateway/routes.parent-alerts-*.json,scripts/test/notification-*.sh,scripts/test/parent-alert-*.sh,docs/qa/notification-*.md,docs/qa/parent-alert-*.md, and rootMakefiletest-notification-*ortest-parent-alert-*targets. docs/qa/notification-route-rehearsals.mdremains static non-default route proof.docs/qa/parent-alert-route-rehearsals.mdadds live Gateway and parent-web browser proof on a non-default table; neither promotes default traffic.- Profile recipient resolution, delivery provider secrets, admin broadcast discovery, default gateway routes, and frontend inbox screens remain orchestrator-owned unless explicitly delegated. Public promotion still requires browser/runtime proof and rollback notes.
Initial tasks:
- Done: workflow tests exist for create/batch/read/delete/preferences and parent alert route coverage. Add new tests only for newly promoted delivery/fanout behavior.
- Done: native producer event fanout accepts
sourceService,sourceEventId,preferenceType, and already-resolved notification recipients onPOST /v1/events/notification. Source-aware batch requests use the same replay-safe, preference-gated fanout path. Add producer-specific tests when exam assigned, grade released, attempt submitted, classroom engagement, support ticket, weak-topic, and admin broadcast adapters are connected. - Done: source event persistence and all preference-approved recipient rows now commit atomically. A fault on a later recipient rolls back the source marker and earlier rows, so an identical producer retry completes the whole fanout once without duplicate inbox rows or retroactive preference changes.
- Done: event transport authentication fails closed before JSON decoding on
POST /v1/events/notificationandPOST /v1/events/weak-topic-alert. Notification events accept onlyadmin-service,attempt-service,classroom-service, andexam-service; weak-topic transport accepts onlybff-servicewhile retaining analytics source metadata in its payload. Notification event payloadsourceServicemust then match its authenticated canonical sender before persistence, preventing replay-namespace spoofing.X-Internal-Tokenis verified in constant time againstNOTIFICATION_INTERNAL_SERVICE_TOKENor sharedINTERNAL_SERVICE_TOKEN. Production-like startup rejects an absent token; a manually unconfigured handler returns stable503 NOTIFICATION_INTERNAL_TOKEN_NOT_CONFIGURED. - Done: native weak-topic parent alert event adapter accepts analytics-produced alert intents on
POST /v1/events/weak-topic-alert, requiressourceService/sourceEventIdplus already-resolvedparentIdsorrecipientIds, appliesWEAK_TOPIC_ALERTpreferences before inbox row creation, and reuses the notification event source store for replay safety. - Add producer event adapter callers for attempt submitted and any remaining support ticket intents. Done: attempt-service native submit/grade can emit
attempt.grade_released.notification.v1events after copied result policy makes the result visible. It sendsGRADE_RELEASEDfor the student andGRADE_RELEASED_PARENTfor parents resolved through profile-service/v1/internal/profiles/parent-recipients/resolve; notification-service remains the replay/preference owner. The same producer contract covers a laterexam.results_released.v1handoff for newly visible, previously hiddenMANUALor pre-closeAFTER_CLOSEattempts. Attempt-service resolves recipients and emits only newly changed attempt source ids; notification-service keeps idempotency, preference, and inbox ownership. Done: exam-service native assignment can emit resolved-recipientexam.assigned.notification.v1events toPOST /v1/events/notificationwhen the internal assignment request supplies student/parent recipient ids. Notification-service remains the replay/preference owner, and exam-service still does not resolve classroom members or parent/student links. Done: classroom-service native quick notification can emitclassroom.notification.sent.v1events for currentSTUDENTclassroom members and, when profile-service is configured, resolved-recipientclassroom.notification.parent.sent.v1events for parent account ids from profile-service/v1/internal/profiles/parent-recipients/resolve. Done: admin-service native explicit-recipient broadcasts can emitadmin.notification.broadcast.v1events for caller-supplied recipient ids. Notification-service remains the event replay/preference owner; admin-service still does not resolveALL/ROLEStargets. Done: admin-service native support workflow producer can emitsupport.ticket.notification.v1events fromPOST /v1/support/notification-eventswhen callers supplyticketId,clientRequestId, concreterecipientIds, and legacy support inbox type values such asSUPPORT_TICKET_CREATED,SUPPORT_TICKET_REPLY, orSUPPORT_TICKET_STATUS. Notification-service remains the inbox, preference, and replay owner; admin-service does not store support tickets or discover recipients. Done: weak-topic callers now have a bounded BFF service-local handoff that accepts analytics intent metadata with concrete student ids, resolves parent recipients through profile-service, and supplies already-resolvedparentIdsto this service's weak-topic event adapter. Public/default gateway cutover remains separate route-proof work. The adapter caller must supply already-resolved recipient ids, org/user/role metadata, preference type, and idempotency keys. - Add an admin inbox/broadcast slice only after recipient search/role broadcast is supplied by profile/IAM/gateway adapters; notification-service stores rows and delivery state, not recipient discovery.
- Done: support queue owner projection exists at
GET /v1/admin/support/notification-summaryfor admin-servicesupport.queuessource snapshots. It counts support notification rows, support notification events, and delivery attempts tied to support notifications only; support ticket storage, recipient discovery, and admin audit history stay outside notification-service. - Done: provider delivery attempt logs are separate from in-app notification state and canonical notification events. Native
POST /v1/delivery-attemptsrecordsQUEUED,SENT,FAILED, andDEAD_LETTEREDattempt status, provider ids, retry timing, error text, and metadata;GET /v1/delivery-attemptsis admin/owner scoped. - Done: the service-local delivery foundation atomically enqueues deterministic per-notification/channel jobs with event fanout. Injected provider adapters claim due jobs with exclusive leases, use stable idempotency keys, recheck mute/type preferences immediately before dispatch, retry transient failures, and dead-letter bounded failures. Provider secrets, concrete production registrations, templates/rendering, and dead-letter operations remain deployment/operator work; the default runtime has no registered provider and cannot claim live delivery.
- Pending canonical delivery slice: production provider registrations and template persistence/rendering for already-resolved recipients. IAM remains the OTP issuer/verifier and may request delivery only through a versioned internal notification event.
- Done: weak-topic/parent alert producer handoff has an internal BFF preview: analytics supplies intent metadata, profile-service resolves parent ids, and notification-service creates replay-safe notifications from the resolved event.
- Done:
make test-notification-event-fanout-postgresstarts a disposable PostgreSQL container and runs the repository transaction test against a temporary schema. It proves a second-recipient write failure rolls back the source event, every inbox row, and every delivery job; retry writes one event, two rows, and two jobs; source replay remains idempotent. The runner writes a reproducible local bundle withsummary.jsonandgo-test.log; latest evidence isoutput/qa/notification-event-fanout-postgres-20260714-174342/.
Acceptance:
- Notification-service receives already-resolved recipients or explicit events.
- Parent-student lookup is not done by DB joins to profile/classroom services.
- Delivery logs are separate from canonical domain events.
- Provider delivery attempts are internal records and do not change inbox read/delete state.
- The default runtime has no provider registration, so this service-local foundation is not evidence of live email, SMS, push, or production delivery.
- Provider template/delivery orchestration stays in notification-service; IAM credentials and OTP verification do not move into this service.
sourceService+sourceEventIdreplay is idempotent for event fanout and does not duplicate inbox rows.- When
AUDIT_SERVICE_URL/AUDIT_SERVICE_BASE_URLis configured, notification-service posts newly persisted notification events to audit-service/v1/audit-eventswithsourceService=notification-service,sourceEventId=<notification event id>,category=DOMAIN,severity=INFO,retentionPolicy=notification-events,metadataSchemaVersion=1, andcreatedAt -> occurredAt. Publish failures are ignored, source event replay does not publish a second audit row, and delivery attempts remain out of scope. - Preferences are applied before provider delivery and are test-covered for muted and disabled notification types.
- Admin broadcast and support notification paths emit audit events through the audit/admin boundary; notification-service does not own admin audit history.
- Support queue projections are read-only owner summaries; they must not become support ticket storage or admin inbox broadcast recipient discovery.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/notification-service/...make test-notification-routes test-parent-alert-routesNOTIFICATION_EVENT_FANOUT_POSTGRES_CONFIRM=disposable-postgres make test-notification-event-fanout-postgresmake test-student-notification-browserfor the memory-only Student inbox and preferences candidate; it is not default-route or full notification-bell parity proof.docs/qa/notification-route-rehearsals.mddocs/qa/parent-alert-route-rehearsals.mddocs/qa/notification-event-fanout-atomicity-proof.md- Current audit tests prove notification event projection redaction, best-effort publishing, and no duplicate audit publish on source replay.
- Future producer/delivery slices must add focused event replay and preference gating tests plus route guards proving broad create/broadcast/provider routes remain legacy until recipient-resolution and delivery adapters are explicit.