feat: notification service

This commit is contained in:
Ilia Denisov
2026-04-22 08:49:45 +02:00
committed by GitHub
parent 5b7593e6f6
commit 32dc29359a
135 changed files with 21828 additions and 130 deletions
+365
View File
@@ -0,0 +1,365 @@
# Notification Service Implementation Plan
This plan has been already implemented and stays here for historical reasons.
It should NOT be threated as source of truth for service functionality.
## Summary
This plan builds `Notification Service` as a durable asynchronous orchestration
service between domain producers, `Gateway`, `Mail Service`, and `User Service`.
The implementation must keep business-audience resolution in the producer,
contact enrichment in `Notification Service`, client push delivery in
`Gateway`, and email execution in `Mail Service`.
## Global Rules
- Keep `Notification Service` orchestration-only.
- Preserve direct auth-code email flow from `Auth / Session Service` to `Mail Service`.
- Use one dedicated Redis Stream for normalized notification intents.
- Keep route retries independent per channel.
- Do not make notification delivery a correctness dependency for gameplay or
geo review state.
- Keep user-facing push payloads lightweight.
## ~~Stage 01.~~ Freeze Vocabulary And Cross-Service Ownership
Status: implemented.
Note:
- Later-stage artifacts may already exist in the repository as draft or
pre-staged documentation.
- Their presence does not mark the corresponding later stages as implemented.
Goal:
- remove ambiguity before runtime work starts
Tasks:
- Freeze `notification:intents` as the dedicated ingress stream.
- Freeze that producers publish concrete `recipient_user_id` values for
user-targeted intents.
- Freeze that `Notification Service` resolves user email and locale from
`User Service`.
- Freeze that admin-only notifications use type-specific configured email
lists.
- Freeze that `template_id == notification_type`.
- Freeze that private-game invites in v1 are user-bound by internal `user_id`.
Exit criteria:
- `ARCHITECTURE.md`, `TESTING.md`, and service READMEs no longer contradict the
agreed notification model
## ~~Stage 02.~~ Define The Intent Contract
Status: implemented.
Goal:
- publish one stable producer-to-notification contract
Tasks:
- Add `notification/api/intents-asyncapi.yaml`.
- Freeze envelope fields:
- `notification_type`
- `producer`
- `audience_kind`
- `recipient_user_ids_json`
- `idempotency_key`
- `occurred_at_ms`
- `request_id`
- `trace_id`
- `payload_json`
- Freeze duplicate and conflict rules on `(producer, idempotency_key)`.
- Freeze `audience_kind=user|admin_email`.
Exit criteria:
- every producer can publish normalized intents without service-specific side
agreements
## ~~Stage 03.~~ Freeze The Notification Catalog
Status: implemented.
Goal:
- turn product decisions into one exact type catalog
Tasks:
- Freeze v1 types and channel matrix.
- Freeze which types are user-targeted versus admin-only.
- Freeze that `lobby.application.submitted` is user-targeted for private games
and admin-email-only for public games.
- Freeze that `lobby.invite.revoked` produces no notification.
- Freeze payload requirements per type.
Exit criteria:
- no notification type remains partially specified
## ~~Stage 04.~~ Define Push Payload Schemas
Status: implemented.
Goal:
- freeze lightweight client-facing payloads
Tasks:
- Add `pkg/schema/fbs/notification.fbs`.
- Define one table per user-facing push type.
- Generate Go bindings under `pkg/schema/fbs/notification`.
- Document the mapping from `notification_type` to FlatBuffers table.
Exit criteria:
- `Gateway` and future client code have one stable schema file for
user-facing notification payloads
## ~~Stage 05.~~ Freeze Mail Template Contracts
Status: implemented.
Goal:
- make email handoff deterministic
Tasks:
- Freeze `payload_mode=template` for notification-generated email.
- Add initial `en` templates for all supported email types in
`mail/templates/<template_id>/en`.
- Update `mail` documentation so notification template IDs align with
`notification_type`.
- Keep `Auth / Session Service` auth-code mail unchanged.
Exit criteria:
- every supported email notification type has a documented template directory
## ~~Stage 06.~~ Define Redis State And Retry Model
Status: implemented.
Goal:
- freeze durable service-local storage before runtime code
Tasks:
- Define `notification_record`, `notification_route`,
`notification_idempotency_record`, `notification_dead_letter_entry`, and
malformed-intent storage.
- Freeze Redis keys and schedule structures.
- Freeze route status vocabulary:
- `pending`
- `published`
- `failed`
- `dead_letter`
- `skipped`
- Freeze retry budgets:
- `push=3`
- `email=7`
Exit criteria:
- the runtime can restart without losing accepted-or-retryable work
## ~~Stage 07.~~ Build The Runnable Service Skeleton
Status: implemented.
Goal:
- create the initial process shape
Tasks:
- Add `cmd/notification`.
- Add `internal/app`, `internal/config`, `internal/api`, `internal/service`,
and `internal/adapters` packages.
- Wire Redis startup checks, graceful shutdown, logger setup, and telemetry.
- Do not add an operator REST API in v1.
Exit criteria:
- the process boots with Redis and configuration validation only
## ~~Stage 08.~~ Implement Intent Acceptance And Idempotency
Status: implemented.
Goal:
- durably accept valid intents and reject invalid or conflicting duplicates
Tasks:
- Consume `notification:intents`.
- Validate the envelope and normalized payload.
- Persist idempotency records and accepted notification records.
- Record malformed intents separately.
- Materialize channel routes according to the type catalog and `audience_kind`.
Exit criteria:
- valid intents are durable and replay-safe before downstream publication begins
## ~~Stage 09.~~ Implement User Enrichment And Locale Resolution
Status: implemented.
Goal:
- make user-targeted routes self-sufficient for later publication
Tasks:
- Read users by `user_id` from `User Service`.
- Extract `email` and `preferred_language`.
- Apply `en` fallback when locale is missing or unsupported.
- Keep admin-email routes independent from `User Service`.
Exit criteria:
- every user-targeted route can be published without additional producer input
## ~~Stage 10.~~ Implement Push Publication
Status: implemented.
Goal:
- hand off user-facing notification events to `Gateway`
Tasks:
- Encode the correct FlatBuffers table per `notification_type`.
- Publish client events into the configured `Gateway` stream with `user_id`
targeting only.
- Apply independent `push` retry policy and route-level dead-letter handling.
Exit criteria:
- user-targeted push notifications survive temporary `Gateway` stream failures
## ~~Stage 11.~~ Implement Mail Publication
Status: implemented.
Goal:
- hand off non-auth email notifications to `Mail Service`
Tasks:
- Build template-mode generic mail commands.
- Set `template_id == notification_type`.
- Pass through normalized template variables from `payload_json`.
- Apply independent `email` retry policy and route-level dead-letter handling.
Exit criteria:
- user and admin email notifications are durably handed off to `Mail Service`
## ~~Stage 12.~~ Integrate Producers
Status: implemented.
Note:
- Implemented as the shared Go producer contract module
`galaxy/notificationintent` because `Game Lobby` and `Geo Profile Service`
code modules are not present in this repository yet.
Goal:
- move upstream services onto the new notification contract
Tasks:
- `Game Master` publishes:
- `game.turn.ready`
- `game.finished`
- `game.generation_failed`
- `Game Lobby` publishes:
- `lobby.runtime_paused_after_start`
- `lobby.application.submitted`
- `lobby.membership.approved`
- `lobby.membership.rejected`
- `lobby.invite.created`
- `lobby.invite.redeemed`
- `lobby.invite.expired`
- `Geo Profile Service` publishes:
- `geo.review_recommended`
- Update `Game Lobby` architecture and later implementation plan to use
user-bound private invites by `user_id`.
Exit criteria:
- producers no longer rely on ad hoc notification-side audience inference
## ~~Stage 13.~~ Add Observability And Recovery Coverage
Status: implemented.
Goal:
- make the async runtime supportable in operations
Tasks:
- Add metrics for intake, duplicates, enrichment, publish attempts, retries,
dead letters, and lag.
- Add structured logging fields shared across intake and route publishers.
- Document manual recovery steps for dead-letter inspection and replay.
Exit criteria:
- the runtime exposes enough signals to detect stuck, noisy, or broken delivery
## ~~Stage 14.~~ Complete Test Coverage And Documentation Alignment
Status: implemented.
Goal:
- close the loop across service tests, boundary tests, and docs
Tasks:
- Add service tests for malformed intents, duplicates, locale fallback, retry
budgets, and route isolation.
- Add inter-service tests with `Gateway`, `Mail Service`, `Game Master`,
`Game Lobby`, and `Geo Profile Service`.
- Update `TESTING.md`.
- Update `ARCHITECTURE.md`, `mail/README.md`, `geoprofile/README.md`, and
gateway examples.
- Verify docs still state that auth-code mail bypasses `Notification Service`.
Exit criteria:
- the implementation and the cross-service documentation describe the same
contracts
## Final Acceptance Criteria
The implementation is complete only when all of the following hold:
- valid intents are consumed from `notification:intents`
- duplicates are idempotent and conflicting duplicates are rejected
- user enrichment resolves email and locale from `User Service`
- `push` and `email` routes are persisted and retried independently
- route dead letters are isolated per channel and per recipient
- `Gateway` fan-out remains user-wide, not session-specific
- `Mail Service` receives template-mode commands whose template IDs match
notification types
- admin notifications remain `email`-only
- auth-code email still bypasses `Notification Service`