Skip to main content

EP-014 — Notification & Communication Engine RTM

EPIC: EP-014 Notification & Communication Engine Reconciled: 2026-05-25 (Wave 1 of 21 per-EPIC scale-out) Pattern: Follows ./PLAYBOOK.md; template is ./EP-002-rtm.md. Source of truth for "% passing": hmp-docs/docs/platform-registry/2026-05-16-13-flow-demo-readiness.md (flow-9 confirmation, flow-10 invoice-email, flow-11 staff-inbox, flow-12 rules, flow-13 whatsapp) until a fresh demo-readiness snapshot is taken.

Status legend​

StatusMeaning
BuiltCode exists + tests exist + tests pass
Built-UntestedCode exists, no automated test coverage (or test exists but is a stub)
PartialCode exists for some acceptance criteria, gaps for others
StubCode is a placeholder / TODO / returns mock data
Not StartedNo code

Summary​

MetricCount
Total stories11
Catalog claims "Built"4 (US-014-004, -006, -010, -011)
Verified-Built2 (US-014-004 template-v2 ships visual editor + 12 tests + ResolverService leak guard; US-014-011 Kafka dispatcher fully shipped)
Built-Untested1 (US-014-006 push — Expo SDK wired, severity-aware FCM routing in Plan C1, but no auto-dispatch trigger covered by tests)
Partial7 (US-014-001 email, -002 sms, -003 triggers, -005 whatsapp, -007 tracking, -009 agent-handoff, -010 preferences)
Stub0
Not Started1 (US-014-008 WhatsApp room-service catalog — catalog service exists but no order-place flow)
Drift findings14 (D-001..D-014; 6 P0 / 6 P1 / 2 P2)
[SME-PROPOSED] refinements32

US-014-001 — Email Delivery via SendGrid (catalog) / Email Notifications (epic body)​

Catalog status: Partial (P0, 5 points) Verified status: Partial — SES (NOT SendGrid) is the production transport since 2026-05-21; webhooks + dual-credential v2 paths added but catalog/EPIC are stale

Code locations:

  • hmp-notification-service/src/channels/email.service.ts — AWS SES SESv2Client + SendEmailCommand (Simple content). Migrated off SendGrid 2026-05-21 for QA PMS-4/PMS-6. IRSA-authenticated, no API key. Boot validator (onModuleInit) hard-fails in prod when SES_FROM_EMAIL is missing (AP-CC-5).
  • hmp-notification-service/src/channels/__tests__/email-v2-integration.spec.ts — provider-config v2 resolver path (PROVIDER_CONFIG_V2_ENABLED gated)
  • hmp-notification-service/src/channels/__tests__/provider-resolver-cache.spec.ts — 5-min resolver cache
  • hmp-notification-service/src/tracking/tracking.controller.ts — POST /email/webhook/sendgrid (P1-C2, ED25519-signed; gated by SENDGRID_WEBHOOK_PUBLIC_KEY)
  • hmp-notification-service/src/tracking/tracking.service.ts — webhook persists delivered/open/bounce/dropped/spamreport/unsubscribe; on hard bounce auto-flips NotificationPreference.optedOut=true for that (guestId, EMAIL)
  • hmp-notification-service/src/dispatcher/dispatcher.service.ts — RESERVATION_CREATED / CHECKED_IN / CHECKED_OUT consumers
  • hmp-notification-service/src/templates/confirmation-letter.ts (en) + confirmation-letter-hi.ts (hi) + -ta.ts (ta) + -te.ts (te) — built-in branded HTML templates
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/providers/email/page.tsx — provider admin UI

Unit + integration tests:

  • hmp-notification-service/src/channels/email.service.spec.ts — 18 specs (mock/SES paths, AP-CC-5 boot validator, attachments-warn, IRSA-only auth)
  • hmp-notification-service/src/tracking/tracking.service.spec.ts — 12 specs (event recording, SendGrid webhook processor, opt-out auto-flip)
  • hmp-notification-service/src/tracking/tracking.controller.spec.ts — controller tests (ED25519 signature verification path)
  • hmp-notification-service/src/templates/confirmation-letter.spec.ts — template snapshot tests
  • hmp-notification-service/src/dispatcher/dispatcher.service.spec.ts — ~50 specs covering reservation lifecycle email auto-send

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/notification-confirmation.flow.spec.ts :: "RESERVATION_CREATED → confirmation email" — TBD (not enumerated in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/email-invoice-delivery.flow.spec.ts :: "checkout → invoice email with PDF attachment" — TBD; AC-02 still gap (no attachments support in SESv2 Simple content) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • (none specific; covered indirectly via reservation real-backend smokes that fire RESERVATION_CREATED)

E2E (Playwright):

  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/providers/email/__tests__/page.test.tsx — provider admin UI tests

Use cases referencing this story:

  • none (gap — no UC YAML for email-confirmation, invoice-email, bounce-handling)

SME notes:

  • Catalog and EPIC body BOTH say "Email Delivery via SendGrid" — but the production code is AWS SES via IRSA (migrated 2026-05-21 per hmp-notification-service/CLAUDE.md AP-CC-5). A new engineer reading the story would look for SendGrid API key configuration that no longer exists. P0 doc-vs-code drift.
  • Indian-hospitality regulatory surface NOT addressed in catalog: e-invoice GST invoice PDF as attachment (per AC-02) is unsupported because SESv2 Simple content cannot carry attachments. Need Raw MIME path before invoice emails can be sent end-to-end.
  • Domain-name / sender-identity SLA: each env has its own verified SES sender (no-reply@mail.{test,qa,staging,keynova}.ai). If DPDPA-compliant consent records require a tenant-specific sender (per the GuestCommunication compliance trail), today only the platform sender is supported.
  • Webhook signature verification (ED25519) is opt-in via SENDGRID_WEBHOOK_PUBLIC_KEY env. Story is silent on whether unsigned webhooks are accepted in non-prod. Today: yes, with a warning log.
  • DLT (TRAI DLT — Telecom Distribution Ledger Technology) registration is for SMS, not email — but SES sender-identity verification + custom MAIL FROM domain SPF/DKIM/DMARC is the equivalent gating step for Indian deliverability. Not enumerated in story.

Drift findings:

D-001 — [P0] [Catalog-vs-code + EPIC-body-vs-code] Catalog title "Email Delivery via SendGrid" and EPIC body ("via SendGrid") both name SendGrid as the transport. Production code uses AWS SES since 2026-05-21 (hmp-notification-service/src/channels/email.service.ts). The email/webhook/sendgrid endpoint kept the SendGrid name because the event-format schema is still SendGrid-compatible — but underlying delivery is SES.

  • [SME-PROPOSED] Rename story to "Email Delivery (AWS SES via IRSA)" and add EPIC-body callout noting the 2026-05-21 migration. Add AC: "Boot SHALL throw AP-CC-5: SES_FROM_EMAIL is required in production when NODE_ENV=production and SES_FROM_EMAIL is missing (bypass via EMAIL_SANDBOX_MODE=true for compose dev-prod mode)."
  • [SME-PROPOSED] Add AC: "Inbound bounce/complaint webhook (currently SendGrid format at /email/webhook/sendgrid) SHALL be replaced or augmented with AWS SNS topic + handler for SES native bounce/complaint events. Hard bounce / complaint MUST flip NotificationPreference.optedOut=true for the recipient with cross-tenant write-prevention derived strictly from the matching NotificationLog row."

D-002 — [P0] [Test-vs-code + India-compliance] AC-02 (GST invoice email with PDF attachment at checkout) is NOT_IMPL per the existing review. The SES migration made this worse: SESv2 Simple content cannot carry attachments at all (CLAUDE.md notes "legacySend path logs a warning and sends the body only when attachments are present; Raw/MIME support is a fast-follow"). E-invoice IRN PDF is mandatory under Indian GST e-invoice mandate (turnover >₹5 cr); silent attachment-drop is a compliance gap.

  • [SME-PROPOSED] Add AC: "When an outbound email request includes an attachments[] payload (e.g. GST invoice PDF), the email service SHALL switch to SESv2 SendEmailCommand with Raw content carrying a MIME-multipart body. If attachments are present and Raw path is not available, the send MUST fail closed (HTTP 503 or NotificationLog status=FAILED with reason ATTACHMENTS_UNSUPPORTED) — never silent-drop."
  • [SME-PROPOSED] Author UC uc-einvoice-pdf-attachment-email.yaml linking this AC to hmp-frontdesk-service checkout flow that owns the IRN PDF generation.

US-014-002 — SMS Delivery via Twilio​

Catalog status: Partial (P1, 5 points) Verified status: Partial — Twilio client shipped + TRAI DND module shipped + provider-config v2 cascade resolver shipped, but story-specified OTP-specific path, DLT template registration, transactional-vs-promotional route distinction, and inbound STOP/START handling are still gaps

Code locations:

  • hmp-notification-service/src/channels/sms.service.ts — Twilio integration with +91 normalization
  • hmp-notification-service/src/channels/simulators/sms.simulator.ts — mock simulator
  • hmp-notification-service/src/compliance/trai-dnd.service.ts — TRAI DND registry lookup (simulator + live modes via @hmp/integrations @Integration decorator)
  • hmp-notification-service/src/tracking/tracking.controller.ts — POST /notifications/webhooks/twilio (no signature verification)
  • hmp-notification-service/src/tracking/tracking.service.ts::processTwilioWebhook — MessageSid/MessageStatus → NotificationLog status mapping
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/providers/sms/page.tsx — SMS provider admin

Unit + integration tests:

  • hmp-notification-service/src/channels/sms.service.spec.ts — ~20 specs (mock + Twilio paths, +91 normalization, errors)
  • hmp-notification-service/src/compliance/trai-dnd.service.spec.ts — DND lookup specs
  • hmp-notification-service/src/tracking/tracking.service.spec.ts — Twilio webhook handling

Flow tests:

  • (no SMS-specific flow test enumerated; covered indirectly when reservation flow includes mobile number) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none

E2E (Playwright):

  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/providers/sms/__tests__/page.test.tsx

Use cases referencing this story: none (gap)

SME notes:

  • DLT registration is a regulatory hard-blocker for Indian SMS, not just a nice-to-have. TRAI requires every Indian telecom marketing/transactional SMS template to be pre-registered on DLT (BSNL/Vodafone-Idea/Jio/Airtel operator platforms). Sending non-registered template content results in operator-level blocking and ₹50K/template fines. Story is silent.
  • Story specifies "transactional vs promotional route distinction" — Twilio's messagingServiceSid pattern is the canonical implementation; today SmsService.send() is single-route.
  • Indian mobile validation (+91[6-9]\d{9}) is partially covered by the +91 normalization helper but not enforced as a rejection at DTO boundary. Sending to +91 5xxx (invalid first-digit) succeeds in code, fails at Twilio with cryptic error.
  • Inbound STOP/START handling (TwiML response) is a TRAI mandate for promotional SMS. Today: no inbound webhook.
  • Time-window enforcement (9 AM – 9 PM IST for promotional) is a TRAI mandate. Today: no enforcement; relies on operator-level enforcement which is unreliable.

Drift findings:

D-003 — [P0] [India-compliance + missing-implementation] Story does not require DLT template registration, TRAI promotional time-window (9 AM – 9 PM IST), or inbound STOP/START handling. All three are TRAI mandates with operator-level enforcement that varies; silent failures result in template blocks. Without DLT pre-registration, every promotional SMS is at risk of being silently blocked by operator filters and the platform incurs ₹50K/template fines on enforcement.

  • [SME-PROPOSED] Add AC: "Marketing-Manager UI SHALL surface a DLT-template-ID field per SMS template. SmsService.send() MUST reject (HTTP 422) any promotional-category send for a template lacking a DLT template ID in production. Transactional category bypasses the check (e.g. OTP) but still requires the DLT-Header-ID for the registered sender (HTLSAP)."
  • [SME-PROPOSED] Add AC: "Promotional-category SMS SHALL be queued to the next 9 AM IST if triggered between 9 PM and 9 AM IST. Quiet-hours check happens at queue-time and survives BullMQ restart."
  • [SME-PROPOSED] Add AC: "Inbound webhook POST /notifications/webhooks/twilio/inbound SHALL handle STOP (flip NotificationPreference.optedOut=true for SMS channel), START (flip back to false), HELP (return help-message). Response SHALL be valid TwiML."

D-004 — [P1] [Story-incomplete + India-compliance] Sender ID (HTLSAP) registration is a TRAI prerequisite for transactional routes but story does not enumerate the per-tenant sender-ID configuration surface. Today: hardcoded TWILIO_PHONE_NUMBER env (single sender across all tenants).

  • [SME-PROPOSED] Add AC: "Per-tenant TRAI-registered sender ID (6-character alphanumeric) SHALL be stored on ProviderCredential (Plan B1 TENANT-scope rows), with validation at credential-create that the sender ID matches ^[A-Z]{2,6}$. Cascade resolver picks per-tenant sender ID before falling back to platform default."

US-014-003 — Automated Trigger Configuration (catalog) / Automated Event Triggers (epic)​

Catalog status: Partial (P0, 5 points) Verified status: Partial — Hardcoded triggers in legacy DispatcherService (4 events) + full RulesV2 engine (Plan D2) with 17 PLATFORM rules seeded + cascade cron + dedup/quota Lua, but RulesV2 is feature-flag OFF in prod; legacy still serving until Plan D5 cutover

Code locations:

  • hmp-notification-service/src/dispatcher/dispatcher.service.ts — Legacy Kafka consumer (RESERVATION_CREATED, CHECKED_IN, CHECKED_OUT, NOTIFICATION_SEND). Has Plan D5 feature-flag fork: short-circuits when RULES_ENGINE_ENABLED=true.
  • hmp-notification-service/src/rules-v2/rules-v2.service.ts — Rule CRUD + version snapshots + publish lifecycle
  • hmp-notification-service/src/rules-v2/rule-executor.service.ts — Event → matching-rule → condition-eval → recipient-resolve → dedup-check → quota-check → render → enqueue
  • hmp-notification-service/src/rules-v2/kafka-subscriber.service.ts — EVENT_NOW / EVENT_OFFSET fan-out
  • hmp-notification-service/src/rules-v2/offset-scheduler.service.ts — BullMQ delayed jobs for offset scheduling
  • hmp-notification-service/src/rules-v2/cron-runner.service.ts — In-process cron for CRON triggers
  • hmp-notification-service/src/rules-v2/broadcast-materializer.service.ts — MANUAL/SCHEDULED broadcast fan-out
  • hmp-notification-service/src/rules-v2/recipient-resolver.service.ts — 5 RecipientSet kinds (GUEST_OF_EVENT, BOOKER_OF_RESERVATION, STAFF_ROLE, STATIC_LIST, SEGMENT)
  • hmp-notification-service/src/rules-v2/meter-counter.service.ts — Atomic Redis Lua quota + dedup primitives
  • hmp-notification-service/src/rules-v2/rules-v2.controller.ts — /api/rules-v2 REST surface (8 rule endpoints + 3 broadcast + 3 rate-limit + 3 audit/replay)
  • hmp-notification-service/prisma/scripts/seed-platform-rules.ts — Seeds 17 PLATFORM rules covering reservation lifecycle, pre-arrival 24h, mid-stay, feedback, loyalty 7d, OTP, payment-received, payment-link-created/expiry, e-invoice IRN, night-audit summary, tenant onboarding welcome
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/rules/page.tsx — Rule list
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/rules/[id]/edit/page.tsx — Rule editor
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/rules/[id]/versions/page.tsx — Version history

Unit + integration tests:

  • hmp-notification-service/src/rules-v2/__tests__/rules-v2.service.spec.ts — CRUD + scope authorization
  • hmp-notification-service/src/rules-v2/__tests__/rule-executor.service.spec.ts — Executor end-to-end
  • hmp-notification-service/src/rules-v2/__tests__/rule-resolver.service.spec.ts — Cascade lookup
  • hmp-notification-service/src/rules-v2/__tests__/recipient-resolver.service.spec.ts — 5 RecipientSet kinds
  • hmp-notification-service/src/rules-v2/__tests__/meter-counter.service.spec.ts — Atomic Lua quota
  • hmp-notification-service/src/rules-v2/__tests__/rate-limit-config.service.spec.ts — Rate-limit CRUD
  • hmp-notification-service/src/rules-v2/__tests__/replay.service.spec.ts — Replay over RuleExecutionLog
  • hmp-notification-service/src/rules-v2/__tests__/scheduled-publish.cron.spec.ts — Scheduled publish cron
  • hmp-notification-service/src/rules-v2/__tests__/feature-flag.spec.ts — Flag gating
  • hmp-notification-service/src/rules-v2/__tests__/rules-v2.controller.spec.ts — REST surface

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/rules-engine.flow.spec.ts :: "rule-driven dispatch (flag ON)" — TBD (not in 13-flow doc; D5 cutover gate) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none

E2E (Playwright): covered via e2e/tests/11-complete-page-inventory.spec.ts indirectly

Use cases referencing this story: none (gap — author UC for cascade-rule-resolution and quota-cap-with-SYSTEM_TRANSACTIONAL-bypass)

SME notes:

  • RulesV2 (Plan D2) is shipped, tested, and seeded but RULES_ENGINE_ENABLED=false in every env as of 2026-05-25. Plan D5 cutover (3 ops tasks: QA soak, staging soak, prod flip) is pending. Story status "Partial" is correct only because the executor's tryEnqueue(...) is a debug-log no-op until D5 wires it to NotificationQueueService to avoid double-send.
  • Legacy DispatcherService is byte-identical because of the feature-flag fork. Test coverage for the legacy path (dispatcher.service.spec.ts ~50 specs) is the source-of-truth for current production behavior.
  • Story does not enumerate the RulesV2 surface (cascade, 5 RecipientSet kinds, dedup, quota, audit/replay). All of these are user-facing concepts the marketing manager will see in the UI. Story body still describes a much simpler "create custom trigger" surface.
  • Indian-compliance angle: MessageCategory enum includes SYSTEM_TRANSACTIONAL (OTP, e-invoice) which bypasses quota caps — this is the right call (regulatory mandates real-time delivery for OTP) but isn't documented in the story.

Drift findings:

D-005 — [P0] [Story-vs-code + scale-mismatch] Story describes a small custom-trigger feature; production code has the full RulesV2 engine (Plan D2: 8 REST endpoints, 17 platform rules seeded, cascade resolver, atomic quota+dedup, replay-over-audit). A new engineer reading the story will undersize the integration test coverage required.

  • [SME-PROPOSED] Rewrite story as "Dispatch Rules Engine v2 (RulesV2)" with sub-ACs covering: (a) cascade lookup PROPERTY→TENANT→PLATFORM, (b) 5 RecipientSet kinds, (c) atomic dedup + quota (with SYSTEM_TRANSACTIONAL bypass), (d) replay over RuleExecutionLog, (e) feature-flag-gated cutover via RULES_ENGINE_ENABLED, (f) co-existence with legacy dispatcher until Plan D5 cutover.
  • [SME-PROPOSED] Add AC: "PII in RuleExecutionLog SHALL be sanitised at write-time — email/phone/firstName/lastName replaced with [redacted]. Per-recipient outcome SHALL be retained for replay but recipient identity SHALL be opaque to non-SUPER_ADMIN viewers. Spec asserts the sanitiser runs on every persisted row."

D-006 — [P1] [Cross-service-handoff] Story doesn't reference how RulesV2 interacts with Plan A3 TemplateV2 (the render orchestrator) or Plan B3 ProviderResolver (the credential cascade). These are 3 separate feature flags (RULES_ENGINE_ENABLED, TEMPLATING_V2_ENABLED, PROVIDER_CONFIG_V2_ENABLED) that interact. Today, only RulesV2 + legacy templates is exercised; the "all three flags ON" matrix is not covered by any test.

  • [SME-PROPOSED] Add AC: "When all three feature flags are ON, the executor SHALL route renders through RenderOrchestratorService (TemplateV2 cascade) and SHALL call ProviderResolver.resolveCredentials for each send. The integration spec at rules-v2/__tests__/integration/all-flags-on.spec.ts (DOES NOT EXIST — author it) MUST cover (event → rule match → render → credential resolve → send) across all 15 v1 event types."

US-014-004 — Template Management with Variables (catalog) / Template Management (epic)​

Catalog status: Built (P0, 3 points) Verified status: Built — Legacy CommunicationTemplate + new TemplateV2 (Plan A3) ship side-by-side; visual editor + MJML compiler + cascade resolver with 5-min Redis cache; cross-tenant denial spec covers ResolverService leak guard

Code locations:

  • hmp-notification-service/src/templates/templates.service.ts — Legacy CommunicationTemplate CRUD with variable substitution
  • hmp-notification-service/src/templates/branding-injector.service.ts — Injects tenant logo / colors / contact into legacy email templates
  • hmp-notification-service/src/templates/template-language-resolver.ts — Resolves multilingual templates (en/hi/ta/te)
  • hmp-notification-service/src/templates-v2/templates-v2.service.ts — New TemplateV2 model with version snapshots + publish lifecycle
  • hmp-notification-service/src/templates-v2/resolver.service.ts — Cascade lookup PROPERTY→TENANT→PLATFORM with 5-min Redis cache + language fallback
  • hmp-notification-service/src/templates-v2/data-loader.service.ts — Per-event render context assembly via ServiceClients
  • hmp-notification-service/src/templates-v2/render-orchestrator.service.ts — Wraps @hmp/template-engine (block-tree → MJML → HTML) for 5 channels
  • hmp-notification-service/src/templates-v2/approved-external-template.service.ts — DLT + Meta WABA hourly sync (mock clients in v1)
  • hmp-notification-service/src/templates-v2/templates-v2.controller.ts — /api/templates-v2 REST surface (gated by TEMPLATING_V2_ENABLED)
  • hmp-shared-packages/packages/notification-contracts/src/events/*.ts — 15 v1 event Zod schemas + 3 fixtures + 5-channel __fallback strings
  • hmp-shared-packages/packages/template-engine/src/*.ts — Block-tree → MJML compiler, 11 typed block types (roomCard / folioTable / qrPass / mjmlPassthrough), Handlebars helpers (currency / IST date / pluralize)
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/templates/page.tsx — Template list
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/templates/[id]/edit/page.tsx — Visual editor
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/templates/[id]/versions/page.tsx — Version history

Unit + integration tests:

  • hmp-notification-service/src/templates/templates.service.spec.ts — 3 specs (legacy)
  • hmp-notification-service/src/templates/branding-injector.service.spec.ts — branding injection
  • hmp-notification-service/src/templates/template-language-resolver.spec.ts — multilingual fallback
  • hmp-notification-service/src/templates/confirmation-letter.spec.ts + night-audit-report.spec.ts — built-in template snapshots
  • hmp-notification-service/src/templates-v2/__tests__/templates-v2.service.spec.ts — TemplateV2 CRUD + publish lifecycle
  • hmp-notification-service/src/templates-v2/__tests__/resolver.service.spec.ts — Cascade + cache + language fallback
  • hmp-notification-service/src/templates-v2/__tests__/render-orchestrator.service.spec.ts — Render pipeline
  • hmp-notification-service/src/templates-v2/__tests__/data-loader.service.spec.ts — Context assembly
  • hmp-notification-service/src/templates-v2/__tests__/approved-external-template.service.spec.ts — DLT/Meta sync
  • hmp-notification-service/src/templates-v2/__tests__/approved-external-template-sync.service.spec.ts — Cron
  • hmp-notification-service/src/templates-v2/__tests__/scheduled-publish.cron.spec.ts — Scheduled-publish cron
  • hmp-notification-service/src/templates-v2/__tests__/metrics.spec.ts — Render metrics
  • hmp-notification-service/src/templates-v2/__tests__/template-variables.controller.spec.ts — Template variables CRUD
  • hmp-notification-service/src/templates-v2/__tests__/templates-v2.controller.spec.ts — REST surface
  • hmp-notification-service/src/templates-v2/__tests__/integration/cascade-permutations.spec.ts — Cascade matrix
  • hmp-notification-service/src/templates-v2/__tests__/integration/full-lifecycle.spec.ts — End-to-end lifecycle
  • hmp-notification-service/src/templates-v2/__tests__/integration/replay.spec.ts — Replay integration
  • hmp-notification-service/src/templates-v2/__tests__/template-v2-cross-tenant-denial.spec.ts — 12 cross-tenant denial specs + ResolverService leak guard (the Gold-Standard Hospitality OS Pick-D Phase-3 fan-out spec for this service; ported 2026-05-24 from hmp-pos-service/src/orders/__tests__/cross-tenant-denial.spec.ts). Orphaned-test status: the spec asserts behaviour that is only partially documented in the story.
  • hmp-shared-packages/packages/notification-contracts/src/__tests__/events/*.spec.ts — Per-event Zod fixture tests (15 events covered)

Flow tests: covered indirectly through notification-confirmation.flow.spec.ts (template render path)

Real-backend smokes: none

E2E (Playwright):

  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/template-variables/__tests__/page.test.tsx

Use cases referencing this story: none (gap — author UC for template cascade with ResolverService leak guard)

SME notes:

  • TemplateV2 (Plan A3) is shipped + tested but TEMPLATING_V2_ENABLED=false in prod as of 2026-05-25. Plan A4 cutover (backfill scripts + dispatcher feature-flag fork) is in place. QA soak protocol documented at docs/templating-v2-cutover-diff-report.md. Story body describes only the legacy CRUD — no mention of TemplateV2.
  • The cross-tenant denial spec (template-v2-cross-tenant-denial.spec.ts) is the canonical security pattern: ResolverService.findById intentionally takes NO tenantId (because PLATFORM-scope templates must cascade to every tenant), so tenant enforcement happens at the controller layer (TemplatesV2Controller.assertReadAuthorization) with ForbiddenException for foreign-tenant reads. 12 tests cover findById/list/create/update/delete/listVersions and the rule-executor render-context leak guard. This is the highest-value security spec in this service; story body should reference it.
  • Multilingual template files (en/hi/ta/te) exist as code-resident templates; TemplateV2 supports the same languages plus dynamic per-tenant overrides. The two paths must produce byte-identical output for the cutover diff-tester at scripts/diff-render-pipelines.ts.
  • Indian-compliance angle: WhatsApp Business templates require Meta WABA approval (24-hour customer-service window rule) — the ApprovedExternalTemplate model tracks approved templates and the cron syncs hourly. SMS templates similarly require DLT approval (D-003). Story doesn't enumerate these external-approval surfaces.

Drift findings:

D-007 — [P1] [Story-vs-code + orphaned-test] Story body describes "rich text editor + variable substitution" — production has the full TemplateV2 stack (cascade, ApprovedExternalTemplate sync, RenderOrchestratorService wrapping @hmp/template-engine, 11 typed block types, 15 v1 event Zod schemas in @hmp/notification-contracts). The cross-tenant denial spec asserts the ResolverService leak guard but the story has no AC for cascade-with-tenant-isolation.

  • [SME-PROPOSED] Add AC: "ResolverService.findById SHALL take NO tenantId (PLATFORM-scope templates cascade to every tenant). Tenant enforcement SHALL happen at TemplatesV2Controller.assertReadAuthorization. Cross-tenant reads MUST throw ForbiddenException — verified by template-v2-cross-tenant-denial.spec.ts (12 specs, orphaned-test status until this AC lands)."
  • [SME-PROPOSED] Add AC: "RenderOrchestratorService SHALL render via @hmp/template-engine for 5 channels (email/sms/whatsapp/push/in-app). Each event in @hmp/notification-contracts (15 v1 events) SHALL have a Zod-validated fixture + 5-channel __fallback string. Failure to render the primary template SHALL fall back to the prior version, then to the event's __fallback."

D-008 — [P2] [Orphaned-code] ApprovedExternalTemplate model + hourly cron exist but the integration with Meta WABA + DLT is mock-only in v1 per CLAUDE.md ("DLT + Meta WABA hourly cron (mock clients in v1; real integrations follow)"). No story owns the live-API integration work.

  • [SME-PROPOSED] Author new story US-014-NEW-DLT-WABA-LIVE "Live DLT + Meta WABA template registry sync" with ACs covering credential management, error handling, and the override-mode for staging where mocks remain.

US-014-005 — WhatsApp Business API Integration​

Catalog status: Partial (P1, 8 points) Verified status: Partial — Full WhatsApp Business module shipped (text/template/interactive/media, flows, catalog, conversations, webhook); ApprovedExternalTemplate sync (mock); Meta verification + production phone-number registration still pending

Code locations:

  • hmp-notification-service/src/channels/whatsapp.service.ts — Core Meta Business API client
  • hmp-notification-service/src/channels/whatsapp-flow.service.ts — Interactive flows (check-in, room service, feedback, wake-up)
  • hmp-notification-service/src/channels/whatsapp-catalog.service.ts — Room service menu catalog
  • hmp-notification-service/src/channels/whatsapp-conversation.service.ts — Conversation history + routing
  • hmp-notification-service/src/channels/whatsapp-webhook.controller.ts — GET /whatsapp/webhook (Meta verification) + POST /whatsapp/webhook (incoming message/status, public)
  • hmp-notification-service/src/whatsapp/whatsapp-business.adapter.ts — High-level adapter
  • hmp-notification-service/src/inbound/* — Plan E2 inbound conversations (gated by INBOUND_V2_ENABLED)
  • hmp-notification-service/src/conversations/* — Plan E2 conversation persistence + reply pipeline (enforces 24h window for non-template messages)
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/providers/whatsapp/page.tsx — WhatsApp provider admin
  • hmp-web-dashboard/src/app/(dashboard)/inbox/page.tsx — Staff inbox

Unit + integration tests:

  • hmp-notification-service/src/channels/whatsapp.service.spec.ts — Core service
  • hmp-notification-service/src/channels/whatsapp-flow.service.spec.ts — Interactive flows
  • hmp-notification-service/src/channels/whatsapp-catalog.service.spec.ts — Catalog
  • hmp-notification-service/src/channels/whatsapp-conversation.service.spec.ts — Conversations
  • hmp-notification-service/src/channels/whatsapp-webhook.controller.spec.ts — Webhook verification
  • hmp-notification-service/src/whatsapp/whatsapp-business.adapter.spec.ts + whatsapp-templates.spec.ts + whatsapp-webhook.controller.spec.ts
  • hmp-notification-service/src/inbound/__tests__/* — 5 specs covering Plan E2 inbound pipeline
  • hmp-notification-service/src/conversations/__tests__/* — 2 specs (conversations.service, reply-pipeline)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/whatsapp-feedback.flow.spec.ts :: "post-stay feedback via WhatsApp" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/staff-inbox.flow.spec.ts :: "staff inbox reply pipeline" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none

E2E (Playwright):

  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/providers/whatsapp/__tests__/page.test.tsx

Use cases referencing this story: none (gap)

SME notes:

  • WhatsApp Business module is deeply integrated but story still says "Not Started" in EPIC body (catalog says "Partial"). The catalog is closer to reality but still understates: there's a full inbound pipeline (Plan E2), staff inbox UI, interactive flows, and catalog. Story body's "Not Started" is severely stale.
  • WhatsApp 24-hour customer-service window rule: outside the 24h window, only Approved External Templates can be sent. Plan E2's ReplyPipelineService enforces this. Story is silent — a new engineer modifying the reply path could easily break the enforcement.
  • DPDPA opt-in: WhatsApp opt-in is required under DPDPA art. 6 (consent). Today: GuestCommunicationPreference.optedIn per channel covers this, but story does not enumerate the opt-in capture point (booking confirmation flow? guest profile?). Risk: implicit opt-in on first WhatsApp send is not DPDPA-compliant.
  • Catalog message + product catalog (interactive list) require Meta WABA "Commerce" approval beyond standard messaging approval. Today the catalog service is plumbed but the Meta-side enablement is operator-only and not surfaced in the story.

Drift findings:

D-009 — [P0] [India-compliance + DPDPA] Story does not specify the explicit WhatsApp opt-in capture point or the consent record format. DPDPA art. 6 requires an unambiguous "free, specific, informed, and unconditional" consent before sending any non-essential WhatsApp message. Today: GuestCommunicationPreference.optedIn is set, but the capture mechanism (a checkbox at booking? a STOP-default-OFF policy?) is undocumented.

  • [SME-PROPOSED] Add AC: "WhatsApp opt-in SHALL be captured at booking-engine confirmation page with an explicit unchecked checkbox 'I agree to receive WhatsApp updates about my stay' + a consent log row with timestamp + IP + reservation ID. Default state is OPT-OUT. Implicit opt-in (e.g. inferring consent from a previous email reply) MUST NOT trigger any WhatsApp send."
  • [SME-PROPOSED] Add AC: "Reply pipeline (ReplyPipelineService) SHALL reject any free-form OUTBOUND WhatsApp message when the conversation's last INBOUND message is older than 24 hours, unless the request includes an approvedTemplateId that resolves to an ACTIVE ApprovedExternalTemplate row. Reject with HTTP 422 + structured error WHATSAPP_24H_WINDOW_EXPIRED."

US-014-006 — Push Notification via Expo (catalog) / Push Notifications (Expo) (epic)​

Catalog status: Built (P1, 3 points) Verified status: Built-Untested — Push service ships severity-aware FCM channel routing (Plan C1), STAFF + GUEST device token registration, registerToken with userType param. Token-registration endpoint exists; but no automated test exercises the auto-dispatch trigger from a Kafka event end-to-end.

Code locations:

  • hmp-notification-service/src/channels/push.service.ts — Push service; severity routing (urgent_priority/default_priority/silent_priority Android channels + iOS interruption-level time-sensitive/active/passive)
  • hmp-notification-service/src/in-app/* — Plan C1 In-App Notifications module (REST + WebSocket); STAFF tokens via JWT_SECRET, GUEST tokens via BOOKING_ENGINE_JWT_SECRET; per-recipient monotonic sequence

Unit + integration tests:

  • hmp-notification-service/src/channels/push.service.spec.ts — Push send specs
  • hmp-notification-service/src/in-app/__tests__/* — 7 specs (controller, integration, service, mute-pref, gateway, recipient-sequence, retention.cron)

Flow tests: none enumerated

Real-backend smokes: none

E2E (Playwright): none

Use cases referencing this story: none (gap)

SME notes:

  • Plan C1 (In-App) is a sibling module — it's about persistent inbox rows + WebSocket; story-006 is about push. Plan C1 extended push.service.ts with severity-aware channel routing. Story should distinguish persistent in-app (Plan C1) from transient push (this story).
  • Story spec says "connected to the guest mobile app (EP-021) for token collection" but token collection happens via registerToken(guestId, token, platform, userType) — STAFF tokens default to STAFF FCM project, GUEST to GUEST project. EP-021 wiring is incomplete (mobile app side).
  • Push for staff/operations alerts (e.g., night-audit-completed, asset-workorder-created, fire alarm) is high-value but story is guest-centric only.

Drift findings:

D-010 — [P1] [Story-incomplete + missing-test] No test exercises the full Kafka-event → trigger-resolution → push-dispatch chain. Story claims "Built" but the auto-dispatch surface is untested. The push.service.spec.ts covers send-once-given-a-token, not the trigger.

  • [SME-PROPOSED] Add AC: "RESERVATION_CHECKED_IN event with pushToken resolved via Plan C1's registerToken MUST emit a push within 30 seconds. Verified by integration spec at dispatcher/__tests__/integration/push-on-checkin.spec.ts (DOES NOT EXIST — author it). Coverage gates Plan C1 cutover."
  • [SME-PROPOSED] Add AC: "URGENT-severity in-app notifications (Plan C1) SHALL bypass guest mute preferences + quiet hours (mirrors the OTP-via-SMS bypass pattern). Spec asserts mute-pref check is skipped when severity=URGENT."

US-014-007 — Delivery Tracking (Sent/Delivered/Read) (catalog) / Delivery Tracking & Analytics (epic)​

Catalog status: Partial (P2, 5 points) Verified status: Partial — NotificationLog status lifecycle (PENDING → QUEUED → SENT → DELIVERED → OPENED) + SendGrid webhook + Twilio callback + WhatsApp webhook persist statuses; per-channel analytics dashboard partially built (delivery counts) but no engagement funnel, no 30-day trend chart

Code locations:

  • hmp-notification-service/src/tracking/tracking.controller.ts — Webhook endpoints
  • hmp-notification-service/src/tracking/tracking.service.ts — Status update + processSendGridWebhook + processTwilioWebhook + getCampaignMetrics
  • hmp-notification-service/src/billing-engine/* — Plan B5 per-channel usage metering (BillingPeriodCounter, MeterFlusher, SlabCompute, PeriodClose)
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/billing-usage/page.tsx — Per-channel usage dashboard
  • hmp-web-dashboard/src/app/(dashboard)/settings/communications/audit/page.tsx — Notification audit log

Unit + integration tests:

  • hmp-notification-service/src/tracking/tracking.service.spec.ts — ~12 specs
  • hmp-notification-service/src/tracking/tracking.controller.spec.ts — controller
  • hmp-notification-service/src/billing-engine/__tests__/* — 11 specs covering meter, period-close, slab-compute, proration, invoice-handoff

Flow tests: none specific

Real-backend smokes: none

E2E (Playwright): none

Use cases referencing this story: none

SME notes:

  • Per-channel breakdown EXISTS via billing-engine (which counts every send by channel/category/scope/recipientType for the 6-segment meter key). Story body's "Aggregate counts, no per-channel breakdown" is stale.
  • Webhook signature verification is fully wired for SendGrid (ED25519) but NOT for Twilio (HMAC) or Meta WABA. Risk: webhook spoofing attacks could trigger fake delivery confirmations or fake opt-outs.
  • Engagement funnel (open-rate, click-rate per template) is implemented in getCampaignMetrics but not surfaced in dashboard UI.

Drift findings:

D-011 — [P1] [Security + missing-implementation] Twilio webhook + Meta WABA webhook do NOT verify HMAC/X-Hub-Signature. SendGrid webhook verifies ED25519 (P1-C2). Inconsistent verification across providers is a webhook-spoofing surface.

  • [SME-PROPOSED] Add AC: "Twilio webhook SHALL verify X-Twilio-Signature HMAC-SHA1 over (URL + sorted body params) using TWILIO_AUTH_TOKEN. Reject with 403 on mismatch. Meta WABA webhook SHALL verify X-Hub-Signature-256 HMAC-SHA256 over raw body using WHATSAPP_APP_SECRET. Reject with 403 on mismatch."

US-014-008 — WhatsApp Room Service Ordering (catalog) / WhatsApp Room Service Catalog (epic body US-014-10)​

Catalog status: Not Started (P3, 8 points) Verified status: Not Started — whatsapp-catalog.service.ts exists (browse menu via interactive list) but no order-place flow, no folio-charge integration, no KOT relay to kitchen

Code locations:

  • hmp-notification-service/src/channels/whatsapp-catalog.service.ts — Catalog message construction (browse only)

Unit + integration tests:

  • hmp-notification-service/src/channels/whatsapp-catalog.service.spec.ts — Catalog construction

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • Browse surface exists, order-confirmation + POS integration (EP-007) does not. F&B menu sync from POS service is not wired.
  • Catalog requires Meta WABA "Commerce" approval beyond standard messaging approval.
  • Story body (US-014-10) says menu items in INR — UPI payment integration is the gap (in addition to folio-charge).

Drift findings: (none net new — Not Started is accurate)

  • [SME-PROPOSED] Defer story to post-pilot. Author UC uc-whatsapp-room-service-order.yaml capturing the menu→order→folio-charge cross-EPIC flow (touches EP-007 POS + EP-005 folio).

US-014-009 — Agent Handoff (Bot to Human) (catalog) / Agent Handoff (WhatsApp/Chat) (epic US-014-09)​

Catalog status: Not Started (P3, 5 points) Verified status: Partial — Plan E2 staff inbox shipped (list/get/reply/assign/resolve/snooze/reopen/search) and /inbox/ dashboard surface lives; routing engine + queue + agent-availability + VIP-first prioritization not built

Code locations:

  • hmp-notification-service/src/conversations/conversations.service.ts — Conversation CRUD
  • hmp-notification-service/src/conversations/reply-pipeline.service.ts — Reply enforcement (24h WA window, cross-channel switch validation)
  • hmp-notification-service/src/inbound/* — Inbound pipeline (token-first → sender-match → unmatched; intent-classifier)
  • hmp-web-dashboard/src/app/(dashboard)/inbox/page.tsx — Staff inbox list
  • hmp-web-dashboard/src/app/(dashboard)/inbox/[conversationId]/page.tsx — Conversation detail
  • hmp-web-dashboard/src/app/(dashboard)/inbox/unmatched/page.tsx — Unmatched sender triage
  • @hmp/inbound-intent package — Heuristic intent classifier (auto-responder detect, keyword scan EN+HI, feedback/complaint/inquiry/OOO intent, 139 tests)

Unit + integration tests:

  • hmp-notification-service/src/conversations/__tests__/conversations.service.spec.ts
  • hmp-notification-service/src/conversations/__tests__/reply-pipeline.service.spec.ts
  • hmp-notification-service/src/inbound/__tests__/* — 5 specs (conversation-persistence, identity-matcher, inbound-event-emitter, intent-classifier, reply-token)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/staff-inbox.flow.spec.ts :: "reply pipeline + identity-matcher" — TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • Catalog says "Not Started" but staff inbox + reply pipeline + intent classifier ARE shipped behind INBOUND_V2_ENABLED flag (default OFF). Plan E2 is feature-complete for v1 (matched/unmatched triage); v2 (routing engine + agent queue + VIP-first) is the gap.
  • Heuristic intent classifier (@hmp/inbound-intent) covers EN + HI keyword scan for feedback/complaint/inquiry/OOO intent + auto-responder detection (139 tests). This is the bot-side of the handoff that the story envisions; story should reference it.
  • VIP-first queue prioritization is a hospitality-specific requirement (VIP guest with billing dispute should jump the queue ahead of a routine inquiry). Today: no queue.

Drift findings:

D-012 — [P0] [Catalog-vs-code] Catalog says "Not Started" for US-014-009; Plan E2 staff inbox is shipped + tested behind INBOUND_V2_ENABLED flag. A reviewer reading the catalog alone would reopen closed work. Same drift pattern as EP-002 D-011 (US-002-007 review-vs-code).

  • [SME-PROPOSED] Promote status to "Partial" and split story into US-014-009a (staff inbox + reply pipeline — SHIPPED) and US-014-009b (routing engine + queue + agent availability + VIP-first — Not Started).
  • [SME-PROPOSED] Add AC for 009b: "VIP-first queue prioritization SHALL sort pending conversations by (guest.vipLevel DESC, conversation.openedAt ASC). Agent console SHALL display queue depth + estimated wait (Little's Law: queueDepth / agentThroughput)."

US-014-010 — Notification Preferences per Guest (catalog) / DND & Opt-Out Management (epic US-014-08)​

Catalog status: Built (P2, 3 points) Verified status: Partial — GuestCommunicationPreference model + per-channel toggles + quiet-hours fields + TRAI DND service shipped; but unsubscribe endpoint exists at /guests/unsubscribe/:token (public, OK), TRAI DND lookup is opt-in via env var, quiet-hours enforcement at send-time is partial

Code locations:

  • hmp-notification-service/src/preferences/comm-preferences.service.ts — Preferences CRUD
  • hmp-notification-service/src/preferences/*.controller.ts — /guests/:guestId/communication-preferences + /guests/unsubscribe/:token
  • hmp-notification-service/src/compliance/trai-dnd.service.ts — TRAI DND registry lookup with simulator/live modes
  • hmp-notification-service/src/in-app/mute-pref.service.ts — In-app mute preferences with quiet-hours (Plan C1)
  • hmp-notification-service/src/consumers/guest-erased.consumer.ts — DPDPA erasure consumer (AP-T08-005)

Unit + integration tests:

  • hmp-notification-service/src/preferences/comm-preferences.service.spec.ts — Preferences CRUD
  • hmp-notification-service/src/compliance/trai-dnd.service.spec.ts — DND lookup
  • hmp-notification-service/src/in-app/__tests__/mute-pref.service.spec.ts — Mute prefs
  • hmp-notification-service/src/consumers/guest-erased.consumer.spec.ts — DPDPA erasure

Flow tests: none specific (DPDPA flow covered separately)

Real-backend smokes: none

Use cases referencing this story: none (gap — author UC for DPDPA-compliant opt-in capture + 90-day PII auto-erasure)

SME notes:

  • DPDPA art. 8 (right to erasure / 90-day PII deletion after stay) is covered by GuestErasedConsumer (AP-T08-005). Replaces NotificationLog.recipient with '[erased]', nulls Guest read-model PII, sets WhatsAppConversation.guestName=null + guestPhone='[erased]' (NOT NULL column). Log rows retained for delivery-count audits. Story does not enumerate this.
  • TRAI DND lookup is via @Integration decorator with simulator/live modes — env-controlled. Story body says "schema exists, TRAI DND check not integrated" but the check IS integrated, just opt-in via TRAI_DND_API_KEY. Production must have this set.
  • Quiet-hours: in-app (Plan C1) enforces via MutePrefService; email/SMS quiet-hours enforcement (per the GuestCommunicationPreference quiet_hours_start/end fields) is partial — not all channels respect it.

Drift findings:

D-013 — [P1] [India-compliance + missing-test] DPDPA erasure path (AP-T08-005 GuestErasedConsumer) is implemented but story doesn't enumerate the cross-service erasure handshake. If the guest-service erasure publisher drops the event silently, notification-service PII is retained indefinitely.

  • [SME-PROPOSED] Add AC: "GuestErasedConsumer SHALL ack via guest.erased.ack event after persisting redactions. guest-service SHALL track unacked erasures and alert ops if any erasure event lacks an ack within 24 hours. Integration spec at consumers/__tests__/integration/erasure-handshake.spec.ts (DOES NOT EXIST — author it) MUST cover the publisher→consumer→ack chain."
  • [SME-PROPOSED] Add AC: "Quiet-hours enforcement SHALL apply uniformly across email/SMS/WhatsApp/push channels (not just in-app). Non-urgent sends triggered during a guest's quiet-hours window SHALL be queued to the next quiet-hours-end timestamp. Urgent-category sends (OTP, e-invoice, fire alarm) bypass."

US-014-011 — Kafka Event-Based Trigger Processing​

Catalog status: Built (P0, 5 points) Verified status: Built — DispatcherService Kafka consumers shipped + tested; multiple consumer modules (folio-settled, tenant-lifecycle, email-verification, asset-wo, asset-alerts, guest-erased) each covered by spec

Code locations:

  • hmp-notification-service/src/dispatcher/dispatcher.service.ts — Central Kafka subscriber
  • hmp-notification-service/src/dispatcher/folio-settled.consumer.ts — Folio-settled email
  • hmp-notification-service/src/dispatcher/tenant-lifecycle.consumer.ts — Tenant onboarding/created events
  • hmp-notification-service/src/dispatcher/email-verification.consumer.ts — Email verification on signup
  • hmp-notification-service/src/consumers/asset-wo.consumer.ts — Asset work-order lifecycle (Plan 5)
  • hmp-notification-service/src/consumers/asset-alerts.consumer.ts — Asset alerts
  • hmp-notification-service/src/consumers/guest-erased.consumer.ts — DPDPA erasure (AP-T08-005)
  • @hmp/messaging — Redis Streams KafkaService wrapper

Unit + integration tests:

  • hmp-notification-service/src/dispatcher/dispatcher.service.spec.ts — ~50 specs
  • hmp-notification-service/src/dispatcher/folio-settled.consumer.spec.ts + tenant-lifecycle.consumer.spec.ts + email-verification.consumer.spec.ts
  • hmp-notification-service/src/consumers/asset-wo.consumer.spec.ts + asset-alerts.consumer.spec.ts + guest-erased.consumer.spec.ts

Flow tests:

  • (covered indirectly via every reservation/folio/asset flow-test that triggers a Kafka event)

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • SKIP_EVENT_SUBSCRIPTIONS=true is per platform CLAUDE.md set to false on every backend service except api-gateway in staging/prod. Notification-service must have SKIP_EVENT_SUBSCRIPTIONS=false for Kafka subscriptions to register.
  • Dedup: today, duplicate Kafka events trigger duplicate notifications because there's no idempotency check at dispatcher entry. RulesV2 (US-014-003) introduces per-rule × recipient × correlationId dedup; legacy dispatcher does not.
  • 6 distinct consumer files (each its own module) — code is well-factored. Story body should reference each consumer as a sub-feature.

Drift findings:

D-014 — [P2] [Cross-cutting + idempotency-gap] Legacy DispatcherService has no event-level idempotency check. Duplicate Kafka events (which happen during consumer-group rebalances) trigger duplicate sends. RulesV2 has per-recipient dedup but legacy dispatcher does not.

  • [SME-PROPOSED] Add AC: "Legacy DispatcherService SHALL maintain a Redis SETNX dedup key per (eventId, eventType) with TTL = 1 hour. Duplicate events MUST log a DISPATCHER_DUPLICATE_EVENT_SKIPPED warning and skip processing. Spec asserts the dedup runs before any send."

RTM file: hmp-docs/docs/requirements/reconciliation/EP-014-rtm.md | Authored 2026-05-25 | Wave 1 of 21 per-EPIC scale-out per PLAYBOOK.md