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
| Status | Meaning |
|---|---|
Built | Code exists + tests exist + tests pass |
Built-Untested | Code exists, no automated test coverage (or test exists but is a stub) |
Partial | Code exists for some acceptance criteria, gaps for others |
Stub | Code is a placeholder / TODO / returns mock data |
Not Started | No code |
Summary
| Metric | Count |
|---|---|
| Total stories | 11 |
| Catalog claims "Built" | 4 (US-014-004, -006, -010, -011) |
| Verified-Built | 2 (US-014-004 template-v2 ships visual editor + 12 tests + ResolverService leak guard; US-014-011 Kafka dispatcher fully shipped) |
| Built-Untested | 1 (US-014-006 push — Expo SDK wired, severity-aware FCM routing in Plan C1, but no auto-dispatch trigger covered by tests) |
| Partial | 7 (US-014-001 email, -002 sms, -003 triggers, -005 whatsapp, -007 tracking, -009 agent-handoff, -010 preferences) |
| Stub | 0 |
| Not Started | 1 (US-014-008 WhatsApp room-service catalog — catalog service exists but no order-place flow) |
| Drift findings | 14 (D-001..D-014; 6 P0 / 6 P1 / 2 P2) |
| [SME-PROPOSED] refinements | 32 |
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 whenSES_FROM_EMAILis 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 cachehmp-notification-service/src/tracking/tracking.controller.ts—POST /email/webhook/sendgrid(P1-C2, ED25519-signed; gated bySENDGRID_WEBHOOK_PUBLIC_KEY)hmp-notification-service/src/tracking/tracking.service.ts— webhook persistsdelivered/open/bounce/dropped/spamreport/unsubscribe; on hard bounce auto-flipsNotificationPreference.optedOut=truefor that (guestId, EMAIL)hmp-notification-service/src/dispatcher/dispatcher.service.ts— RESERVATION_CREATED / CHECKED_IN / CHECKED_OUT consumershmp-notification-service/src/templates/confirmation-letter.ts(en) +confirmation-letter-hi.ts(hi) +-ta.ts(ta) +-te.ts(te) — built-in branded HTML templateshmp-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 testshmp-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.mdAP-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
Simplecontent cannot carry attachments. NeedRawMIME 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_KEYenv. 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 productionwhenNODE_ENV=productionandSES_FROM_EMAILis missing (bypass viaEMAIL_SANDBOX_MODE=truefor 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 flipNotificationPreference.optedOut=truefor 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 SESv2SendEmailCommandwithRawcontent 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 reasonATTACHMENTS_UNSUPPORTED) — never silent-drop." - [SME-PROPOSED] Author UC
uc-einvoice-pdf-attachment-email.yamllinking this AC tohmp-frontdesk-servicecheckout 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 normalizationhmp-notification-service/src/channels/simulators/sms.simulator.ts— mock simulatorhmp-notification-service/src/compliance/trai-dnd.service.ts— TRAI DND registry lookup (simulator + live modes via@hmp/integrations@Integrationdecorator)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 mappinghmp-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 specshmp-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
messagingServiceSidpattern 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/inboundSHALL handleSTOP(flipNotificationPreference.optedOut=truefor 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 whenRULES_ENGINE_ENABLED=true.hmp-notification-service/src/rules-v2/rules-v2.service.ts— Rule CRUD + version snapshots + publish lifecyclehmp-notification-service/src/rules-v2/rule-executor.service.ts— Event → matching-rule → condition-eval → recipient-resolve → dedup-check → quota-check → render → enqueuehmp-notification-service/src/rules-v2/kafka-subscriber.service.ts— EVENT_NOW / EVENT_OFFSET fan-outhmp-notification-service/src/rules-v2/offset-scheduler.service.ts— BullMQ delayed jobs for offset schedulinghmp-notification-service/src/rules-v2/cron-runner.service.ts— In-process cron for CRON triggershmp-notification-service/src/rules-v2/broadcast-materializer.service.ts— MANUAL/SCHEDULED broadcast fan-outhmp-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 primitiveshmp-notification-service/src/rules-v2/rules-v2.controller.ts—/api/rules-v2REST 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 welcomehmp-web-dashboard/src/app/(dashboard)/settings/communications/rules/page.tsx— Rule listhmp-web-dashboard/src/app/(dashboard)/settings/communications/rules/[id]/edit/page.tsx— Rule editorhmp-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 authorizationhmp-notification-service/src/rules-v2/__tests__/rule-executor.service.spec.ts— Executor end-to-endhmp-notification-service/src/rules-v2/__tests__/rule-resolver.service.spec.ts— Cascade lookuphmp-notification-service/src/rules-v2/__tests__/recipient-resolver.service.spec.ts— 5 RecipientSet kindshmp-notification-service/src/rules-v2/__tests__/meter-counter.service.spec.ts— Atomic Lua quotahmp-notification-service/src/rules-v2/__tests__/rate-limit-config.service.spec.ts— Rate-limit CRUDhmp-notification-service/src/rules-v2/__tests__/replay.service.spec.ts— Replay over RuleExecutionLoghmp-notification-service/src/rules-v2/__tests__/scheduled-publish.cron.spec.ts— Scheduled publish cronhmp-notification-service/src/rules-v2/__tests__/feature-flag.spec.ts— Flag gatinghmp-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=falsein 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'stryEnqueue(...)is a debug-log no-op until D5 wires it toNotificationQueueServiceto avoid double-send. - Legacy
DispatcherServiceis 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:
MessageCategoryenum includesSYSTEM_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
RuleExecutionLogSHALL 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 substitutionhmp-notification-service/src/templates/branding-injector.service.ts— Injects tenant logo / colors / contact into legacy email templateshmp-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 lifecyclehmp-notification-service/src/templates-v2/resolver.service.ts— Cascade lookup PROPERTY→TENANT→PLATFORM with 5-min Redis cache + language fallbackhmp-notification-service/src/templates-v2/data-loader.service.ts— Per-event render context assembly via ServiceClientshmp-notification-service/src/templates-v2/render-orchestrator.service.ts— Wraps@hmp/template-engine(block-tree → MJML → HTML) for 5 channelshmp-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-v2REST surface (gated byTEMPLATING_V2_ENABLED)hmp-shared-packages/packages/notification-contracts/src/events/*.ts— 15 v1 event Zod schemas + 3 fixtures + 5-channel__fallbackstringshmp-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 listhmp-web-dashboard/src/app/(dashboard)/settings/communications/templates/[id]/edit/page.tsx— Visual editorhmp-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 injectionhmp-notification-service/src/templates/template-language-resolver.spec.ts— multilingual fallbackhmp-notification-service/src/templates/confirmation-letter.spec.ts+night-audit-report.spec.ts— built-in template snapshotshmp-notification-service/src/templates-v2/__tests__/templates-v2.service.spec.ts— TemplateV2 CRUD + publish lifecyclehmp-notification-service/src/templates-v2/__tests__/resolver.service.spec.ts— Cascade + cache + language fallbackhmp-notification-service/src/templates-v2/__tests__/render-orchestrator.service.spec.ts— Render pipelinehmp-notification-service/src/templates-v2/__tests__/data-loader.service.spec.ts— Context assemblyhmp-notification-service/src/templates-v2/__tests__/approved-external-template.service.spec.ts— DLT/Meta synchmp-notification-service/src/templates-v2/__tests__/approved-external-template-sync.service.spec.ts— Cronhmp-notification-service/src/templates-v2/__tests__/scheduled-publish.cron.spec.ts— Scheduled-publish cronhmp-notification-service/src/templates-v2/__tests__/metrics.spec.ts— Render metricshmp-notification-service/src/templates-v2/__tests__/template-variables.controller.spec.ts— Template variables CRUDhmp-notification-service/src/templates-v2/__tests__/templates-v2.controller.spec.ts— REST surfacehmp-notification-service/src/templates-v2/__tests__/integration/cascade-permutations.spec.ts— Cascade matrixhmp-notification-service/src/templates-v2/__tests__/integration/full-lifecycle.spec.ts— End-to-end lifecyclehmp-notification-service/src/templates-v2/__tests__/integration/replay.spec.ts— Replay integrationhmp-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 fromhmp-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=falsein prod as of 2026-05-25. Plan A4 cutover (backfill scripts + dispatcher feature-flag fork) is in place. QA soak protocol documented atdocs/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
ApprovedExternalTemplatemodel 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-enginefor 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__fallbackstring. 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 clienthmp-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 cataloghmp-notification-service/src/channels/whatsapp-conversation.service.ts— Conversation history + routinghmp-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 adapterhmp-notification-service/src/inbound/*— Plan E2 inbound conversations (gated byINBOUND_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 adminhmp-web-dashboard/src/app/(dashboard)/inbox/page.tsx— Staff inbox
Unit + integration tests:
hmp-notification-service/src/channels/whatsapp.service.spec.ts— Core servicehmp-notification-service/src/channels/whatsapp-flow.service.spec.ts— Interactive flowshmp-notification-service/src/channels/whatsapp-catalog.service.spec.ts— Cataloghmp-notification-service/src/channels/whatsapp-conversation.service.spec.ts— Conversationshmp-notification-service/src/channels/whatsapp-webhook.controller.spec.ts— Webhook verificationhmp-notification-service/src/whatsapp/whatsapp-business.adapter.spec.ts+whatsapp-templates.spec.ts+whatsapp-webhook.controller.spec.tshmp-notification-service/src/inbound/__tests__/*— 5 specs covering Plan E2 inbound pipelinehmp-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
ReplyPipelineServiceenforces 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.optedInper 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 anapprovedTemplateIdthat resolves to an ACTIVEApprovedExternalTemplaterow. Reject with HTTP 422 + structured errorWHATSAPP_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 specshmp-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.tswith 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
pushTokenresolved via Plan C1'sregisterTokenMUST emit a push within 30 seconds. Verified by integration spec atdispatcher/__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 endpointshmp-notification-service/src/tracking/tracking.service.ts— Status update + processSendGridWebhook + processTwilioWebhook + getCampaignMetricshmp-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 dashboardhmp-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 specshmp-notification-service/src/tracking/tracking.controller.spec.ts— controllerhmp-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
getCampaignMetricsbut 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-SignatureHMAC-SHA1 over (URL + sorted body params) usingTWILIO_AUTH_TOKEN. Reject with 403 on mismatch. Meta WABA webhook SHALL verifyX-Hub-Signature-256HMAC-SHA256 over raw body usingWHATSAPP_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.yamlcapturing 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 CRUDhmp-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 listhmp-web-dashboard/src/app/(dashboard)/inbox/[conversationId]/page.tsx— Conversation detailhmp-web-dashboard/src/app/(dashboard)/inbox/unmatched/page.tsx— Unmatched sender triage@hmp/inbound-intentpackage — 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.tshmp-notification-service/src/conversations/__tests__/reply-pipeline.service.spec.tshmp-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_ENABLEDflag (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 CRUDhmp-notification-service/src/preferences/*.controller.ts—/guests/:guestId/communication-preferences+/guests/unsubscribe/:tokenhmp-notification-service/src/compliance/trai-dnd.service.ts— TRAI DND registry lookup with simulator/live modeshmp-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 CRUDhmp-notification-service/src/compliance/trai-dnd.service.spec.ts— DND lookuphmp-notification-service/src/in-app/__tests__/mute-pref.service.spec.ts— Mute prefshmp-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
@Integrationdecorator with simulator/live modes — env-controlled. Story body says "schema exists, TRAI DND check not integrated" but the check IS integrated, just opt-in viaTRAI_DND_API_KEY. Production must have this set. - Quiet-hours: in-app (Plan C1) enforces via
MutePrefService; email/SMS quiet-hours enforcement (per the GuestCommunicationPreferencequiet_hours_start/endfields) 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.ackevent after persisting redactions. guest-service SHALL track unacked erasures and alert ops if any erasure event lacks an ack within 24 hours. Integration spec atconsumers/__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 subscriberhmp-notification-service/src/dispatcher/folio-settled.consumer.ts— Folio-settled emailhmp-notification-service/src/dispatcher/tenant-lifecycle.consumer.ts— Tenant onboarding/created eventshmp-notification-service/src/dispatcher/email-verification.consumer.ts— Email verification on signuphmp-notification-service/src/consumers/asset-wo.consumer.ts— Asset work-order lifecycle (Plan 5)hmp-notification-service/src/consumers/asset-alerts.consumer.ts— Asset alertshmp-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 specshmp-notification-service/src/dispatcher/folio-settled.consumer.spec.ts+tenant-lifecycle.consumer.spec.ts+email-verification.consumer.spec.tshmp-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=trueis per platform CLAUDE.md set tofalseon every backend service except api-gateway in staging/prod. Notification-service must haveSKIP_EVENT_SUBSCRIPTIONS=falsefor 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_SKIPPEDwarning 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