Files
goodgo-platform/apps/api
Ho Ngoc Hai f70d7e3deb feat(notifications): pilot listener cutover + consumer skeleton (GOO-173)
Phase 1 steps 4–6 — cutover PaymentCompletedListener to the async
outbox path behind NOTIFICATIONS_ASYNC_ENABLED, and add the Redis
Streams consumer that picks up notification.requested events.

PaymentCompletedListener changes:
- Injects NotificationsAsyncConfig + NotificationsPublisher
- When asyncEnabled: builds NotificationRequestedPayload and calls
  publisher.publishStandalone() instead of commandBus.execute()
- Legacy SendNotificationCommand path retained in else branch
- recipientEmail passed via params so the consumer can resolve it

NotificationsConsumer (new):
- XREADGROUP against `events:notification.requested` stream,
  consumer group `notifications-workers`
- Idempotency via Redis SET NX EX 86400 keyed on
  envelope.payload.dedupeKey ?? envelope.eventId
- Dispatches to existing SendNotificationHandler per channel via
  CommandBus, mapping contract channels (email/sms/fcm/zalo/in_app)
  to domain channels (EMAIL/SMS/PUSH/ZALO_OA)
- DLQ: after 3 failed deliveries, XADD to
  events:notification.requested:dlq with original envelope + reason
- Consumer group created lazily with MKSTREAM; poll loop gated by
  NOTIFICATIONS_ASYNC_ENABLED
- Registered in NotificationsModule providers

Tests (28 specs, all green):
- PaymentCompletedListener: legacy path, async path, skip-when-no-email
  (4 specs, updated from 3 to match new 5-arg constructor)
- NotificationsConsumer: process message, dedupe skip, missing envelope
  skip, DLQ after max retries, multi-channel dispatch, empty stream
  (6 specs)
- NotificationsPublisher: 4 specs (unchanged)
- NotificationsAsyncConfig: 14 specs (unchanged)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 18:28:29 +07:00
..