Skip to content

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-contract work only: services/attempt-service/**, services/attempt-service/api/openapi.yaml, contracts/openapi/services/attempt-service.yaml, and docs/api/attempt-service.md.
  • contracts/openapi/services/notification-service.yaml
  • docs/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 root Makefile test-notification-* or test-parent-alert-* targets.
  • docs/qa/notification-route-rehearsals.md remains static non-default route proof. docs/qa/parent-alert-route-rehearsals.md adds 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 on POST /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/notification and POST /v1/events/weak-topic-alert. Notification events accept only admin-service, attempt-service, classroom-service, and exam-service; weak-topic transport accepts only bff-service while retaining analytics source metadata in its payload. Notification event payload sourceService must then match its authenticated canonical sender before persistence, preventing replay-namespace spoofing. X-Internal-Token is verified in constant time against NOTIFICATION_INTERNAL_SERVICE_TOKEN or shared INTERNAL_SERVICE_TOKEN. Production-like startup rejects an absent token; a manually unconfigured handler returns stable 503 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, requires sourceService/sourceEventId plus already-resolved parentIds or recipientIds, applies WEAK_TOPIC_ALERT preferences 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.v1 events after copied result policy makes the result visible. It sends GRADE_RELEASED for the student and GRADE_RELEASED_PARENT for parents resolved through profile-service /v1/internal/profiles/parent-recipients/resolve; notification-service remains the replay/preference owner. The same producer contract covers a later exam.results_released.v1 handoff for newly visible, previously hidden MANUAL or pre-close AFTER_CLOSE attempts. 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-recipient exam.assigned.notification.v1 events to POST /v1/events/notification when 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 emit classroom.notification.sent.v1 events for current STUDENT classroom members and, when profile-service is configured, resolved-recipient classroom.notification.parent.sent.v1 events for parent account ids from profile-service /v1/internal/profiles/parent-recipients/resolve. Done: admin-service native explicit-recipient broadcasts can emit admin.notification.broadcast.v1 events for caller-supplied recipient ids. Notification-service remains the event replay/preference owner; admin-service still does not resolve ALL/ROLES targets. Done: admin-service native support workflow producer can emit support.ticket.notification.v1 events from POST /v1/support/notification-events when callers supply ticketId, clientRequestId, concrete recipientIds, and legacy support inbox type values such as SUPPORT_TICKET_CREATED, SUPPORT_TICKET_REPLY, or SUPPORT_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-resolved parentIds to 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-summary for admin-service support.queues source 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-attempts records QUEUED, SENT, FAILED, and DEAD_LETTERED attempt status, provider ids, retry timing, error text, and metadata; GET /v1/delivery-attempts is 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-postgres starts 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 with summary.json and go-test.log; latest evidence is output/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 + sourceEventId replay is idempotent for event fanout and does not duplicate inbox rows.
  • When AUDIT_SERVICE_URL/AUDIT_SERVICE_BASE_URL is configured, notification-service posts newly persisted notification events to audit-service /v1/audit-events with sourceService=notification-service, sourceEventId=<notification event id>, category=DOMAIN, severity=INFO, retentionPolicy=notification-events, metadataSchemaVersion=1, and createdAt -> 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-routes
  • NOTIFICATION_EVENT_FANOUT_POSTGRES_CONFIRM=disposable-postgres make test-notification-event-fanout-postgres
  • make test-student-notification-browser for the memory-only Student inbox and preferences candidate; it is not default-route or full notification-bell parity proof.
  • docs/qa/notification-route-rehearsals.md
  • docs/qa/parent-alert-route-rehearsals.md
  • docs/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.

Go-platform documentation is generated from repository Markdown.