Skip to main content

EP-004 — Guest Management RTM

EPIC: EP-004 Guest Management Reconciled: 2026-05-24 (Wave 1 of RTM Reconciliation Scale-Out) Pattern: derived from pilot EP-002-rtm.md; see ./PLAYBOOK.md. Numbering note: the EPIC body and the catalog (01-epic-catalog.md lines 108-123) assign DIFFERENT titles to the same US-004-NNN ids from -002 onward. This RTM follows the catalog titles per Wave 1 task scoping. The drift is recorded as D-001.

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 stories (this RTM scope, US-004-001 .. US-004-013)13
Catalog claims "Built"10
Catalog claims "Partial"3 (US-004-008 Communication Log, US-004-012 Consent Mgmt, US-004-013 Doc Upload)
Verified-Built8 (US-004-001, -002, -003, -004, -005, -006, -009, -010)
Built-Untested1 (US-004-007 VIP — implicit via guest CRUD tests; no dedicated assertion)
Partial4 (US-004-008 Comm Log; US-004-011 GDPR Export; US-004-012 Consent; US-004-013 Doc Upload)
Stub0
Not Started0 (all 13 in-scope stories have code)
Drift findings12 (D-001..D-012; 4 P0 / 6 P1 / 2 P2)
[SME-PROPOSED] refinements28

US-004-001 — Guest Profile CRUD​

Catalog status: Built (P0, 5 points) Verified status: Built

Code locations:

  • hmp-guest-service/src/guests/guests.service.ts — Guests domain service (1000+ lines incl. Aadhaar masking + Verhoeff validator)
  • hmp-guest-service/src/guests/guests.controller.ts:36-152 — POST /guests, GET /guests, GET /properties/:propertyId/guests, GET /guests/:id, PUT /guests/:id, DELETE /guests/:id (soft delete; @Audit on writes)
  • hmp-guest-service/src/guests/dto/guest.dto.ts — CreateGuestDto, UpdateGuestDto, GuestResponseDto with @Sensitive/DataScope decorators (Aadhaar in idNumber, etc.)
  • hmp-guest-service/prisma/schema.prisma — Guest model with @@unique([propertyId, email]), vipLevel (0-5), guestType, totalStays/totalRevenue, plus DPDPA §9 fields (guardianName/guardianRelation/guardianPhone/guardianConsentAt) — see guest-service CLAUDE.md Gotcha 9
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/page.tsx — guest list page
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx — guest detail
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/keynova.tsx — KeyNova-themed wrapper

Unit + integration tests:

  • hmp-guest-service/src/guests/guests.service.spec.ts — CRUD + Aadhaar Verhoeff + PAN regex + sensitive-data masking
  • hmp-guest-service/src/guests/guests.controller.spec.ts — route declaration order pin (qa-PMS-9) + RBAC fan-out
  • hmp-guest-service/src/guests/__tests__/guest-cross-tenant-denial.spec.ts — 13-test tenant-isolation suite (Wave 1 Task #2; verify this is REGISTERED in test runner — [CITED-BUT-NOT-FOUND] whether it's wired into the default Jest pattern or is an orphaned spec; see D-002)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 1 (create) + Scenario 2 (update) — pass-rate TBD (not enumerated in 2026-05-16-13-flow-demo-readiness.md) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/dpdpa-children-consent.flow.spec.ts — guardian consent on under-18 creation; pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-crm.real.spec.ts — CRM landing + guest list with seeded data
  • hmp-web-dashboard/e2e/real-backend/smoke-guest-search.real.spec.ts — guest typeahead/list

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guests.spec.ts — guest list + create
  • hmp-web-dashboard/e2e/tests/crm-guest-detail.spec.ts — guest detail page
  • hmp-web-dashboard/e2e/tests/crm-guardian-fields.spec.ts — guardian-consent UI (DPDPA §9)
  • hmp-web-dashboard/e2e/tests/01-page-inventory.spec.ts, 08-data-integrity.spec.ts — broader smoke coverage

Use cases referencing this story:

  • none enumerated (Wave 1 source-map gap; sidecars in Wave 2)

SME notes:

  • Existing review (reviews/EP-004/US-004-001-review.md) is ~10 weeks stale (2026-03-07) and verdicts "PARTIAL — Add Guest button has no form". The dashboard now ships a complete create/edit flow per the keynova page; the review should be refreshed.
  • DPDPA §9 guardian-consent gates are enforced at create-time (Gotcha 9 — BadRequestException for missing guardianName/guardianRelation/guardianPhone if DOB < 18). Story body does NOT mention this regulation. New engineers will not know why a create can 400 on "missing fields" that are not in the AC.
  • Email uniqueness is per-property (@@unique([propertyId, email]), Gotcha 2). Story's Scenario 3 ("Prevent duplicate email within same property") correctly states this — good. The cross-property duplicate-email case is silently allowed; needs an explicit AC + suggest-merge UX.
  • Aadhaar Verhoeff (Gotcha 15) — validateIdFormat runs on every AADHAAR ID number. Test-author hygiene mandates UIDAI test numbers 234123412346 or 999941057058 — NOT invented 12-digit strings.
  • AES-256-GCM encryption on GuestIdentity.idNumber via FieldEncryptionService is shipped per review file; story doesn't enumerate it.

Drift findings:

D-001 — [P0] [Catalog-vs-EPIC-body] The catalog (01-epic-catalog.md lines 108-123) titles stories US-004-002 onward differently from the EPIC body (epics/EP-004-guest-management.md). Examples:

  • Catalog -002 = "Guest Search"; EPIC body -002 = "Guest 360-Degree View"
  • Catalog -003 = "360-Degree View"; EPIC body -003 = "Identity Management"
  • Catalog -006 = "Deduplication & Merge"; EPIC body -006 = "VIP Level" This is structurally the same drift class as EP-002 D-007 — the catalog is the source of truth in this RTM (per Wave 1 task), so EPIC body §US-004-002..§US-004-013 needs realignment.
  • [SME-PROPOSED] Adopt the catalog numbering, rewrite EPIC body §US-004-002..§US-004-013 to match, and add a > Previously titled "X" in EPIC body draft prior to 2026-05-24 reconciliation alias line under each section. Same fix shape as EP-002 D-007.

D-002 — [P1] [Test-vs-code] hmp-guest-service/src/guests/__tests__/guest-cross-tenant-denial.spec.ts (13 tests, Wave 1 Task #2) is in a __tests__/ subfolder. Verify it is picked up by the default Jest testMatch (typical **/*.spec.ts) — if Jest is configured to scan only src/**/*.spec.ts non-recursively, the spec is orphaned. The 12 other guest-service spec files all live alongside their service files (not in __tests__/).

  • [SME-PROPOSED] Run npx jest --listTests --testPathPattern='guest-cross-tenant' in CI and assert the spec resolves. If orphaned, either move it to src/guests/guest-cross-tenant-denial.spec.ts (sibling) or add <rootDir>/src/**/__tests__/**/*.spec.ts to jest.config.ts.testMatch. Capture the discovery in a 1-line Gotcha.

US-004-002 — Guest Search (Name, Phone, Email)​

Catalog status: Built (P0, 5 points) Verified status: Built

Code locations:

  • hmp-guest-service/src/guests/guests.controller.ts:80-105 — GET /guests/cross-property (tenant-wide, MANAGER+), GET /guests/search-phone (tenant-wide phone search), POST /guests/search (filter DTO)
  • hmp-guest-service/src/guests/guests.controller.ts:47-72 — GET /guests?search=<q> typeahead (list endpoint with search query param, case-insensitive contains over firstName/lastName/email/phone)
  • hmp-guest-service/src/guests/guests.service.ts — findAll(), search(), searchByPhone(), crossPropertySearch()
  • hmp-guest-service/src/guests/dto/guest.dto.ts — SearchGuestsDto
  • hmp-guest-service/src/sync/fuzzy-match.service.ts — Jaro-Winkler scorer (CITED — used by DeduplicationService but NOT wired into POST /guests/search per US-004-004 review; see D-003)
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/page.tsx — search input
  • hmp-web-dashboard/src/app/(dashboard)/chain/guests/page.tsx — chain-level cross-property search
  • hmp-web-dashboard/src/components/modules/reservations/guest-search.tsx — reservation-form typeahead combobox

Unit + integration tests:

  • hmp-guest-service/src/guests/guests.service.spec.ts — search variants + indexing
  • hmp-guest-service/src/sync/guest-sync.service.spec.ts — fuzzy-match scoring inside the dedup pipeline

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 1 indirectly exercises search (search after create) — pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-guest-search.real.spec.ts — guest search against seeded backend

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guests.spec.ts — search box
  • hmp-web-dashboard/e2e/reservation-flow.spec.ts — uses guest-search combobox

Use cases referencing this story:

  • none enumerated

SME notes:

  • Route declaration order is load-bearing (Gotcha 14): GET /guests/cross-property and GET /guests/search-phone MUST be declared BEFORE GET /guests/:id (which carries ParseUUIDPipe). The controller spec asserts this via PATH_METADATA reflection — good defense.
  • There is no GET /guests/search route — only POST /guests/search. Plain typeahead uses GET /guests?search=<q>. This is a frontend integration trap not captured in the story.
  • Indian-hospitality phone-number normalization matters: a guest may search 9876543210 while the stored value is +91-9876543210. The catalog story does not specify normalization. The existing review file (US-004-004 in pre-reconciliation numbering, now US-004-002 per catalog) confirms basic substring match works but flags @IsEmail blocking partial email matches.
  • Cross-property search is gated to MANAGER+ via @Roles('SUPER_ADMIN','ADMIN','MANAGER') — story doesn't enumerate RBAC. Front-desk staff who legitimately need to find a returning guest from another property in the same chain currently get 403.
  • Fuzzy matching (Jaro-Winkler in FuzzyMatchService) exists but is NOT wired into the POST /guests/search endpoint — only into the dedup pipeline. This is a P1 UX gap.

Drift findings:

D-003 — [P1] [Story-incomplete + UX-gap] FuzzyMatchService (Jaro-Winkler) exists and is exercised by dedup, but is NOT wired into the operator-facing POST /guests/search endpoint. The story body promises "fuzzy search by name" (Scenario 2) but the endpoint is exact-substring only. Returning-guest lookup at check-in suffers misspellings ("Vikrm" → no match).

  • [SME-PROPOSED] Add AC: "POST /guests/search SHALL apply Jaro-Winkler similarity (threshold 0.85) on firstName + lastName when fuzzy=true query param is set. Results SHALL include a matchScore field. Exact-match results SHALL be returned first, fuzzy below them."
  • [SME-PROPOSED] Add AC: "Phone-number search SHALL normalize country-code variants: input 9876543210 matches stored +919876543210, +91-9876543210, 9876543210. Helper: normalizePhone(input) strips +, -, spaces, and country code if length > 10."

D-004 — [P1] [Story-incomplete + RBAC-gap] Cross-property search is restricted to MANAGER+; front-desk staff at chain hotels routinely need to find returning guests across sister properties. Catalog story does not enumerate the RBAC ladder.

  • [SME-PROPOSED] Add AC: "GET /guests/cross-property SHALL be accessible to FRONT_DESK and RESERVATIONS roles when the user has membership in ≥2 properties for the tenant. RBAC check: Roles(...) OR (tenant.propertyCount > 1 AND user.roles ∋ FRONT_DESK|RESERVATIONS)."

US-004-003 — 360-Degree Guest View​

Catalog status: Built (P0, 8 points) Verified status: Built (backend); dashboard Partial — 3 of 6 tabs implemented per reviews/EP-004/US-004-002-review.md (stale numbering)

Code locations:

  • hmp-guest-service/src/guests/guests.controller.ts:195-204 — GET /guests/:id/360
  • hmp-guest-service/src/guests/guests.controller.ts:184-193 — GET /guests/:id/history (subset)
  • hmp-guest-service/src/guests/guests.service.ts:get360View() — aggregates: profile + identities + addresses + preferences + reservations (last 10) + communications (last 10) + feedbackResponses (last 5) + loyalty (with tier + transactions); SEC-PRIV-04+07 Aadhaar audit log fires when 360 view exposes AADHAAR identity
  • hmp-guest-service/src/read-model-sync/read-model-sync.service.ts — inbound sync of Reservation/Folio/Property from event bus (per CLAUDE.md ReadModelSyncModule)
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx — guest detail page (uses useGuest(id) + useGuestHistory(id), NOT the unified /360 endpoint per review file — see D-005)

Unit + integration tests:

  • hmp-guest-service/src/guests/guests.service.spec.ts — get360View() aggregation paths + Aadhaar access audit
  • hmp-guest-service/src/read-model-sync/read-model-sync.service.spec.ts — Reservation/Folio inbound projection

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenarios 3-5 indirectly exercise identity/address/preference round-trip that feeds 360 view; explicit 360 assertion pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-crm.real.spec.ts — guest detail page renders

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guest-detail.spec.ts — detail tabs

Use cases referencing this story:

  • none enumerated

SME notes:

  • Backend 360 returns 7 sections (profile, identities, addresses, preferences, reservations, communications, feedbackResponses, loyalty); frontend renders 3 tabs (Stays, Preferences, Documents) per the stale review. Loyalty + Communications + Feedback tabs are missing from the UI.
  • AC Scenario 2 demands "< 1 second" load + "API < 800 ms" but there is no LATENCY assertion in any spec or flow test today. The aggregation across 5+ tables for a guest with 50+ communications WILL exceed 800 ms on cold cache.
  • The dashboard fetches GET /guests/:id + GET /guests/:id/history separately instead of calling /guests/:id/360. This doubles network round-trips and means the operator sees a partial view if either call fails.
  • Read-model staleness: communications are projected via event bus (no synchronous HTTP); a campaign sent 60 s ago may not appear yet. Story doesn't explain the eventual-consistency window.

Drift findings:

D-005 — [P1] [Dashboard-vs-backend integration] Dashboard guest detail calls GET /guests/:id + GET /guests/:id/history separately, not the unified GET /guests/:id/360. As a result Loyalty / Communications / Feedback tabs are absent in the UI even though the backend ships the data.

  • [SME-PROPOSED] Add AC: "Dashboard guest detail page SHALL call GET /guests/:id/360 as the primary fetch and render six tabs: Profile, Stays, Preferences, Identity, Communications, Loyalty. useGuest(id) SHALL be replaced by useGuest360(id)."
  • [SME-PROPOSED] Add AC: "360 endpoint p95 latency SHALL be < 800 ms for guests with ≤20 reservations + ≤50 communications. Add a flow-test assertion that measures and fails the build above 1500 ms (3× ceiling) under seeded canary load."

US-004-004 — Identity Document Management (6 doc types)​

Catalog status: Built (P0, 5 points) Verified status: Built (backend); dashboard Partial per reviews/EP-004/US-004-003-review.md (stale numbering)

Code locations:

  • hmp-guest-service/src/guests/guests.controller.ts:206-217 — POST /guests/:id/identities (FRONT_DESK+)
  • hmp-guest-service/src/guests/guests.service.ts:validateIdFormat() lines 105-122 — Aadhaar regex ^\d{12}$ + PAN regex ^[A-Z]{5}\d{4}[A-Z]$ + Aadhaar Verhoeff checksum
  • hmp-guest-service/src/guests/guests.service.ts:verhoeffValidate() lines 124-176 — UIDAI Verhoeff (Gotcha 15)
  • hmp-guest-service/src/guests/guests.service.ts:maskAadhaar() lines 44-100 — XXXX XXXX 1234 mask + SEC-PRIV-04 audit log on full-Aadhaar decrypt access
  • hmp-guest-service/src/guests/dto/guest.dto.ts:36-83 — IdType enum (PASSPORT/AADHAAR/DRIVING_LICENSE/VOTER_ID/PAN_CARD/NATIONAL_ID/OTHER — 7 values; story says 6, the 7th OTHER was the pre-AP-T06-002 hack for visas — see D-006), AddIdentityDto
  • hmp-guest-service/src/guests/guest-visa.controller.ts — POST/GET/PUT /guests/:id/visas (AP-T06-002 — replaces idType='OTHER' for foreign-guest visa data)
  • hmp-guest-service/src/guests/guest-visa.service.ts — VisaService with tenant-scope + xPrisma any-cast (Gotcha 12)
  • hmp-guest-service/prisma/schema.prisma — GuestIdentity (idType/idNumber/issueDate/expiryDate/isPrimary/isVerified/documentUrl) + GuestVisa (visaNumber/visaType/Form-C movement fields)
  • hmp-guest-service/prisma/migrations/20260523000000_guest_visa_frro_fields/migration.sql — Visa schema migration (Gotcha 12 — idempotent)
  • hmp-guest-service/prisma/migrations-data/migrate-guest-identity-visa.sql — data migration from idType='OTHER' to guest_visas
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx — Documents tab (Partial UI per stale review)

Unit + integration tests:

  • hmp-guest-service/src/guests/guests.service.spec.ts — Aadhaar Verhoeff (use UIDAI test numbers per Gotcha 15) + PAN regex + masking
  • hmp-guest-service/src/guests/guest-visa.service.spec.ts — visa CRUD + tenant scope + cross-tenant denial

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 3 (attach identity + address) — pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/flow-6-check-in.spec.ts :: 6/13 PASS (Aadhaar/C-Form scenarios PENDING) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/cform-foreigner-reg.flow.spec.ts :: Form-C / FRRO submission using GuestVisa data — pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none specific (covered via check-in smoke + guest-detail e2e)

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guest-detail.spec.ts — Documents tab presence

Use cases referencing this story:

  • AP-T06-002 (visa CRUD; data migration) — referenced in guest-service CLAUDE.md Gotcha 12

SME notes:

  • The Aadhaar masking + Verhoeff + SEC-PRIV-04 audit-log stack is the most India-compliance-mature code in the platform. UIDAI requires masking in display (last 4 visible) — implemented at lines 89-101. Story body Scenario 1 references the mask format XXXX XXXX 1234 but does NOT make masking a hard AC ("should be masked in display as" is hedged language). The audit log on full-Aadhaar access (SEC-PRIV-04) is not in the story at all.
  • 6 ID types in story, 7 in enum (OTHER is the pre-AP-T06-002 hack used for visa data; now superseded by the dedicated GuestVisa model). The 7th value should be removed or marked deprecated.
  • Document upload via S3 presigned URLs (uploads.controller.ts) is shipped but encryption-at-rest (SSE-KMS) on the S3 bucket is unverified per EP-004 gap analysis. This is a US-004-013 concern but appears here because guests' Aadhaar scans land in the same bucket.
  • Frontend "Documents" tab uses a stale type set (passport/id_card/driving_license/other) per the stale review — does not match the 6 backend types. Frontend cannot create Aadhaar identity rows today.
  • Identity duplicate prevention: per stale review, the backend does NOT check for duplicate (guestId, idType, idNumber) — an operator could create three Aadhaar rows for the same guest.

Drift findings:

D-006 — [P0] [Compliance-critical + story-incomplete] Aadhaar masking + SEC-PRIV-04 audit-log on full-decrypt access ARE shipped but the story body uses hedged language ("should be masked in display as") and does not require the audit log. UIDAI guidelines + DPDPA Act 2023 §8(4) mandate access logging — a regression could remove it silently.

  • [SME-PROPOSED] Add AC: "Aadhaar idNumber SHALL be returned in masked form (XXXX XXXX <last4>) for ALL roles except SUPER_ADMIN. SUPER_ADMIN full-decrypt access SHALL write an AuditLog row (event='AADHAAR_FULL_ACCESS', actorUserId, guestId, at). Spec covers both masked default and SUPER_ADMIN audit-write path."
  • [SME-PROPOSED] Add AC: "(guestId, idType, idNumber) SHALL be unique. Duplicate add returns 409 with code=DUPLICATE_IDENTITY. Migration adds @@unique([guestId, idType, idNumber]) to GuestIdentity."
  • [SME-PROPOSED] Add AC: "idType enum SHALL be exactly the 6 catalog types (PASSPORT, AADHAAR, DRIVING_LICENSE, VOTER_ID, PAN_CARD, NATIONAL_ID). OTHER is REMOVED — foreign visa data SHALL live in the dedicated GuestVisa model (AP-T06-002, Gotcha 12). Data migration migrate-guest-identity-visa.sql MUST be run on every environment before the enum drop lands."

US-004-005 — Guest Preference Management​

Catalog status: Built (P0, 5 points) Verified status: Built (backend); dashboard Partial per reviews/EP-004/US-004-005-review.md (model mismatch)

Code locations:

  • hmp-guest-service/src/preferences/preferences.controller.ts:27-119 — 8 endpoints: GET all, GET by category, POST upsert single, PUT bulk, DELETE by key, GET /preferences/common, GET/PUT communication-preferences
  • hmp-guest-service/src/preferences/preferences.service.ts — 5 categories: ROOM, FOOD, SERVICE, COMMUNICATION, ACCESSIBILITY
  • hmp-guest-service/src/preferences/dto/preference.dto.ts — DTOs + PreferenceCategory enum
  • hmp-guest-service/prisma/schema.prisma — GuestPreference (@@unique([guestId, category, key])) + GuestCommunicationPreference (per channel × TRANSACTIONAL/PROMOTIONAL/FEEDBACK/LOYALTY)
  • hmp-web-dashboard/src/app/(dashboard)/guests/[guestId]/preferences/page.tsx — preferences page
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx — Preferences tab (uses flat model — does NOT consume category-based API per stale review)

Unit + integration tests:

  • hmp-guest-service/src/preferences/preferences.service.spec.ts — CRUD + bulk + common catalog

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 4 (set & read room preference) — pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none specific

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guest-detail.spec.ts — Preferences tab

Use cases referencing this story: none enumerated

SME notes:

  • 23 predefined keys in /preferences/common per stale review — solid catalog. Story Scenario 3 enumerates several but does not bind to the actual returned catalog.
  • Frontend uses flat fields (roomPreference, floorPreference, smokingPreference, pillowType, dietaryRestrictions[], specialRequests) that do NOT match the backend's category-based key-value model. Either the frontend is consuming a different API entirely, or there's an adapter layer translating — review file flags model mismatch.
  • Communication preferences (4 categories TRANSACTIONAL/PROMOTIONAL/FEEDBACK/LOYALTY) overlap conceptually with Guest.marketingConsent/dataConsent booleans AND with consent records (US-004-012). Three sources of truth for "can we email this guest a campaign?" is a P0 compliance risk under DPDPA + TRAI DND.

Drift findings:

D-007 — [P0] [Compliance-critical + duplicate-state] Consent state lives in THREE places: Guest.marketingConsent/dataConsent (booleans), GuestCommunicationPreference (channel × category × optedIn), and ConsentLog/ConsentRecord (US-004-012 audit trail). TRAI DND + DPDPA require a SINGLE source of truth — sending a marketing SMS to a guest whose Guest.marketingConsent=true but GuestCommunicationPreference{channel=SMS, category=PROMOTIONAL, optedIn=false} is a violation.

  • [SME-PROPOSED] Add AC: "Outbound dispatch (notification-service) SHALL check GuestCommunicationPreference as the authoritative consent gate. Guest.marketingConsent SHALL be a DERIVED column refreshed via DB trigger or service-side hook from the per-channel × per-category preference matrix. Conflict (Guest=true, pref=false) blocks the send AND emits consent.conflict.detected for ops."

D-008 — [P1] [Frontend-vs-backend model drift] Dashboard Preferences tab uses flat fields that do not map to the backend's category-based API. Operators editing a preference in the UI may write to the wrong shape or fail silently.

  • [SME-PROPOSED] Add AC: "Preferences tab SHALL render a per-category section (ROOM/FOOD/SERVICE/COMMUNICATION/ACCESSIBILITY), populated by calling GET /guests/:guestId/preferences (grouped response). Edits SHALL POST to /guests/:guestId/preferences per key. The legacy flat fields are removed from the dashboard form model."

US-004-006 — Guest Deduplication & Merge​

Catalog status: Built (P1, 5 points) [shown as Built in catalog line 113] Verified status: Built (backend); dashboard Not Implemented per reviews/EP-004/US-004-006-review.md

Code locations:

  • hmp-guest-service/src/guests/guests.controller.ts:165-182 — POST /guests/merge (MANAGER+, @Audit), POST /guests/merge-preview
  • hmp-guest-service/src/guests/guests.service.ts:merge() — atomic Prisma transaction: transfers reservations + dedups identities (by encrypted comparison) + dedups preferences (by category:key) + transfers addresses + aggregates stats + soft-deletes secondaries + publishes Kafka guest.merged
  • hmp-guest-service/src/guests/guest-merge.service.spec.ts — 20+ merge specs
  • hmp-guest-service/src/sync/deduplication.service.ts — findDuplicates() (cross-property) + getMergeSuggestions() (within-property) using FuzzyMatchService (Jaro-Winkler)
  • hmp-guest-service/src/sync/fuzzy-match.service.ts — Jaro-Winkler scorer
  • @hmp/infra DistributedLockModule — used to prevent concurrent merges of the same guest profiles (Gotcha 6)
  • Frontend: NONE FOUND — no merge UI per stale review

Unit + integration tests:

  • hmp-guest-service/src/guests/guest-merge.service.spec.ts — atomic merge + identity dedup + Kafka emit
  • hmp-guest-service/src/sync/guest-sync.service.spec.ts — dedup + fuzzy scoring
  • hmp-guest-service/src/guests/__tests__/guest-cross-tenant-denial.spec.ts — covers merge cross-tenant denial

Flow tests: NONE FOUND specifically for merge

Real-backend smokes: none

E2E (Playwright): NONE FOUND for merge UI

Use cases referencing this story: none enumerated

SME notes:

  • Backend merge is production-grade: atomic transaction, encryption-aware identity dedup, distributed lock, Kafka emit for downstream invalidation. Reviewers consistently mark backend as fully implemented.
  • Frontend merge UI is completely absent — there is no merge-suggestion page, no merge confirmation dialog, no preview-then-commit flow. The catalog calls this "Built" because the backend works, but operators cannot reach the feature.
  • US-004-014 (catalog row 122) "Automated Dedup Suggestions" is Not Started per catalog, but DeduplicationService.getMergeSuggestions() already implements this — only the cron + UI wrapper is missing. The two stories overlap.
  • Merge consequences are PROFOUND (irreversible, cascades to reservations + folios + identities). No "undo within N hours" path. A wrong merge across two real guests with active stays is unrecoverable.

Drift findings:

D-009 — [P0] [Built-without-UI = operator-can't-use] Catalog marks Built but the frontend has no merge UI at all. Production hotels cannot merge duplicates today. The "Built" label is misleading.

  • [SME-PROPOSED] Demote catalog status Built → Partial until the dashboard ships: (a) merge suggestions page at /crm/guests/duplicates consuming getMergeSuggestions(), (b) preview-then-commit dialog calling /guests/merge-preview then /guests/merge, (c) audit-trail link to the resulting guest.merged Kafka event.
  • [SME-PROPOSED] Add AC: "Merge SHALL support an UNDO window of 24h: the deactivated secondary profiles SHALL be retained (soft-deleted) and a POST /guests/merge/:mergeId/undo endpoint SHALL reverse the merge if called within 24h. Beyond 24h the operation is permanent."

US-004-007 — VIP Level Management (4 tiers)​

Catalog status: Built (P1, 2 points) [catalog line 114] Verified status: Built-Untested — covered implicitly via guest CRUD tests; no dedicated assertion

Code locations:

  • hmp-guest-service/prisma/schema.prisma — Guest.vipLevel integer field (CLAUDE.md says 0-5; story says 0-3 — see D-010)
  • hmp-guest-service/src/guests/dto/guest.dto.ts — vipLevel in CreateGuestDto/UpdateGuestDto with @IsInt() @Min(0) @Max(5) (matching schema)
  • hmp-guest-service/src/guests/guests.service.ts — updates flow via standard PUT /guests/:id
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/page.tsx — VIP badge column
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx — VIP badge on detail
  • Front-desk arrivals board ([CITED-BUT-NOT-FOUND] — specific file not enumerated in source map; check hmp-web-dashboard/src/app/(dashboard)/frontdesk/arrivals)

Unit + integration tests:

  • Implicit only — no dedicated vipLevel spec in guests.service.spec.ts

Flow tests: NONE FOUND for VIP-specific assertion

Real-backend smokes: none specific

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guests.spec.ts — may render VIP column

Use cases referencing this story: none enumerated

SME notes:

  • Catalog vs CLAUDE.md drift on tier count: catalog/EPIC body say 4 tiers (0=None, 1=VIP, 2=VVIP, 3=Celebrity); guest-service CLAUDE.md says vipLevel range 0-5. The schema validator allows 0-5 but UI/story stop at 3. The 4-and-5 tiers are silently writable through the API but render no badge.
  • VIP alert UX (Scenario 2) requires the alert text on the check-in screen — depends on frontdesk-service surfacing it, which is cross-EPIC (EP-005A). No test covers the cross-service handoff.
  • Indian-hospitality VIP triggers often include "this guest brought 8 corporate bookings last quarter" (corporate-relationship VIP) or "guest is a film celebrity with paparazzi risk" (privacy-VIP) — single integer ladder is too flat. Day-2 enhancement: structured vipBasis (CORPORATE_VOLUME | LOYALTY_TIER | CELEBRITY | RELIGIOUS | DIGNITARY).

Drift findings:

D-010 — [P1] [Schema-vs-story drift] Schema accepts vipLevel 0-5; catalog + story enumerate 0-3 (4 tiers). Levels 4 and 5 are unreachable from UI but reachable from API, producing guests with invalid tiers.

  • [SME-PROPOSED] Add AC: "Either (a) enforce vipLevel ∈ {0,1,2,3} at DTO level (Max=3), OR (b) extend catalog/story to 6 tiers (0=None, 1=VIP, 2=VVIP, 3=Celebrity, 4=Dignitary, 5=Owner-Family). Choose one; update Guest.vipLevel Max(N) and badge UI to match."
  • [SME-PROPOSED] Add AC: "Add a dedicated vip-classification.spec.ts asserting: (a) updating vipLevel writes AuditLog row; (b) Guest.alerts text SHALL be displayed on the arrivals board for vipLevel ≥ 2; (c) VIP-only fields (e.g. preferredRoom) are exposed only to roles MANAGER+."

US-004-008 — Guest Communication Log (Aggregated)​

Catalog status: Partial (P1, 5 points) [catalog line 115] Verified status: Partial — model exists; cross-service aggregation incomplete

Code locations:

  • hmp-guest-service/prisma/schema.prisma — GuestCommunication (channel EMAIL/SMS/WHATSAPP/PUSH, direction, status, templateId) per CLAUDE.md
  • hmp-guest-service/src/guests/guests.service.ts:get360View() — pulls last 10 communications into 360 response
  • Inbound projection from notification-service: [CITED-BUT-NOT-FOUND] — no dedicated CommunicationProjectionConsumer; the EP-004 gap analysis explicitly flags "communications from notification-service / WhatsApp / POS receipts are not aggregated into this model"
  • hmp-notification-service/src/... — owns dispatch; does NOT emit a canonical communication.dispatched event consumed by guest-service (per EP-004 gap analysis claim)

Unit + integration tests:

  • Implicit only — no dedicated communication-projection spec

Flow tests: NONE FOUND specific to comm-log aggregation

Real-backend smokes: none

Use cases referencing this story: none enumerated

SME notes:

  • Catalog correctly marks "Partial" — the gap is real per EP-004 §Gap Analysis ("Medium impact — CRM managers can't see the full communication picture").
  • POS receipts (printed at folio close — see @hmp/common/printing ESC/POS generators) and OTP SMS via notification-service are NOT projected. The 360 view's Communications tab is therefore systematically incomplete.
  • A consumer here is the canonical pattern: every service that dispatches guest-facing communication MUST publish a communication.dispatched event with (guestId, channel, status, templateId, at) consumed by guest-service.

Drift findings:

D-011 — [P1] [Cross-service-handoff + story-incomplete] No service emits communication.dispatched to guest-service; comms tab is systematically incomplete. CRM cannot demonstrate marketing-pressure compliance to DPDPA auditors without it.

  • [SME-PROPOSED] Add AC: "hmp-notification-service SHALL publish communication.dispatched event on every send (EMAIL/SMS/WHATSAPP/PUSH) with payload {tenantId, propertyId, guestId, channel, direction='OUTBOUND', status, templateId, at}. hmp-guest-service SHALL consume via a CommunicationProjectionConsumer and upsert a GuestCommunication row."
  • [SME-PROPOSED] Add AC: "POS receipt printing (hmp-pos-service) SHALL publish communication.dispatched with channel='PRINT' for in-person receipts. Folio printing (hmp-frontdesk-service) same pattern."
  • [SME-PROPOSED] Add AC: "360 view Communications tab SHALL render a CHANNEL filter (EMAIL/SMS/WHATSAPP/PUSH/PRINT/IN_APP) and a date-range filter. Beyond first 10, paginate via ?cursor=."

US-004-009 — Guest Address Management (3 types)​

Catalog status: Built (P1, 2 points) [catalog line 116] Verified status: Built

Code locations:

  • hmp-guest-service/src/guests/guests.controller.ts:219-230 — POST /guests/:id/addresses (FRONT_DESK+, @Audit)
  • hmp-guest-service/src/guests/dto/guest.dto.ts:46-130 — AddressType enum (HOME/WORK/BILLING/OTHER — 4 values; catalog story says 3), AddAddressDto, GuestAddressDto
  • hmp-guest-service/prisma/schema.prisma — GuestAddress (addressType/line1/line2/city/state/country/postalCode/isPrimary)
  • hmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx — addresses section (within Profile tab)

Unit + integration tests:

  • Implicit via guests.service.spec.ts — no dedicated addAddress block enumerated

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 3 (attach identity + address) — pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none specific

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guest-detail.spec.ts — profile + address render

Use cases referencing this story: none enumerated

SME notes:

  • Catalog says "3 types"; enum has 4 (HOME/WORK/BILLING/OTHER). The 4-vs-3 mismatch echoes US-004-007 D-010.
  • Indian GST e-invoice (US-004-016 / catalog -016) needs the BILLING address attached to the linked Company row, not the Guest — the AC Scenario 1 conflates the two. Corporate billing addresses correctly belong to Company.address (Company model in guest-service per CLAUDE.md).
  • isPrimary per addressType means a guest can have 1 HOME + 1 WORK + 1 BILLING marked primary simultaneously — the constraint is per-type, not per-guest. Story body Scenario 1 implies "primary" is global; clarify.

Drift findings: (none net new beyond D-010 enum-count pattern — same shape)

  • [SME-PROPOSED] Add AC: "Either enforce addressType ∈ {HOME, WORK, BILLING} (drop OTHER) to match the catalog 3-types contract, OR extend the story to 4 types with explicit OTHER use case. Choose one."
  • [SME-PROPOSED] Add AC: "isPrimary SHALL be unique per (guestId, addressType): exactly one HOME may be primary, etc. Setting a new primary auto-unsets the prior primary in a transaction."
  • [SME-PROPOSED] Add AC: "When addressType = BILLING is set on a guest with companyId IS NOT NULL, the API SHALL warn BILLING_ADDRESS_SHOULD_BE_ON_COMPANY because GST e-invoices for corporates use Company.address."

US-004-010 — GDPR Right to Erasure​

Catalog status: Built (P0, 5 points) [catalog line 117 — note P0 not P1; EPIC body line 64 says P1 — see D-001 cascade] Verified status: Built

Code locations:

  • hmp-guest-service/src/gdpr/gdpr.controller.ts:24-38 — DELETE /gdpr/guests/:id/erasure (ADMIN+, @Audit, x-tenant-id required)
  • hmp-guest-service/src/gdpr/gdpr.service.ts — eraseGuestData() anonymizes PII (firstName/lastName → "REDACTED", email/phone/dateOfBirth/anniversary → null, identities deleted, addresses deleted, preferences deleted), preserves financial records
  • hmp-guest-service/src/gdpr/gdpr.service.spec.ts — erasure spec
  • hmp-guest-service/src/gdpr/checkout-consumer.service.ts — CheckoutConsumerService (RESERVATION_CHECKED_OUT → creates GuestRetentionTimer with expiresAt = checkoutDate + 90d) — Gotcha 11
  • hmp-guest-service/src/gdpr/retention-cron.service.ts — RetentionCronService runs at 02:10 daily; finds expired timers; auto-erases with legalBasis='RETENTION_EXPIRY' (AP-T08-004) — Gotcha 11
  • hmp-guest-service/src/gdpr/data-retention.service.ts — 3-year lastStay purge service
  • 8 service-side guest-erased.consumer.ts consumers (cascade): hmp-frontdesk-service/src/consumers/guest-erased.consumer.ts, hmp-reservation-service/src/guest-erased/guest-erased.consumer.ts, hmp-pos-service/src/consumers/, hmp-housekeeping-service/src/consumers/, hmp-notification-service/src/consumers/, hmp-crm-service/src/consumers/, hmp-spa-service/src/consumers/, hmp-payment-service/src/consumers/, hmp-analytics-service/src/consumers/ — all have .spec.ts co-located (verified via Grep)
  • hmp-shared-packages/packages/messaging/src/events/event-types.ts — guest.erased event type definition
  • hmp-web-dashboard/src/app/(dashboard)/settings/compliance/page.tsx — compliance settings (erasure UI hosted via DSAR flow)
  • hmp-web-dashboard/src/app/(dashboard)/crm/dsar/page.tsx, crm/dsar/[id]/page.tsx — DSAR lifecycle UI

Unit + integration tests:

  • hmp-guest-service/src/gdpr/gdpr.service.spec.ts
  • hmp-guest-service/src/gdpr/checkout-consumer.service.spec.ts
  • hmp-guest-service/src/gdpr/data-retention.service.spec.ts
  • hmp-guest-service/src/gdpr/retention-cron.service.spec.ts
  • 8 service-side guest-erased.consumer.spec.ts files

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 6 (erasure anonymises PII) — pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none specific

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-dsar-lifecycle.spec.ts — DSAR submit → process → complete

Use cases referencing this story:

  • AP-T08-004 (90-day retention timer) — referenced in guest-service CLAUDE.md Gotcha 11

SME notes:

  • This is the most cross-service-complete feature in EP-004. 8 consumer services + checkout consumer + retention cron + dedicated GuestRetentionTimer model + GdprErasureLog with legalBasis enum (RIGHT_TO_ERASURE for manual / RETENTION_EXPIRY for cron) — solid architecture per Gotcha 11.
  • DPDPA §8(7) requires erasure on guest request; 90-day post-checkout auto-erasure (cron) is stricter than required but matches "data minimization" principle.
  • Erasure preserves financial records (folio amounts, reservation dates) — correct for GST audit retention (7 years per CBIC) — but story does not enumerate which fields survive vs which die.
  • The cascade across 8 services means erasure is eventually consistent. A guest whose erasure JUST fired may still appear unredacted in analytics-service for ≤ event-bus replication lag. The story does not specify a max-lag SLO.

Drift findings: (none net new — implementation outstrips story)

  • [SME-PROPOSED] Add AC: "Erasure cascade SHALL complete within 5 seconds p95 across all 8 downstream services. Add a flow-test assertion that subscribes to guest.erased, fires erasure, and asserts every consumer reaches processedAt IS NOT NULL within 5 s."
  • [SME-PROPOSED] Add AC: "Erasure-preserved fields SHALL be enumerated explicitly: Folio.totalAmount, Folio.gstAmount, Reservation.checkInDate, Reservation.checkOutDate, Reservation.roomNumber, Reservation.roomTypeCode (denormalized), and the foreign-key Reservation.guestId (now pointing at the REDACTED guest row). Everything else SHALL be nulled or hashed."

US-004-011 — GDPR Data Portability Export​

Catalog status: Built (P1, 3 points) [catalog line 118] Verified status: Partial — endpoint exists; format + completeness gaps

Code locations:

  • hmp-guest-service/src/gdpr/gdpr.controller.ts:40-52 — GET /gdpr/guests/:id/export (ADMIN+)
  • hmp-guest-service/src/gdpr/gdpr.service.ts:exportGuestData() — returns profile + identities + addresses + preferences + stay history + communications + loyalty as JSON

Unit + integration tests:

  • hmp-guest-service/src/gdpr/gdpr.service.spec.ts — export shape spec

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 5 (GDPR Art. 20 export returns full record set) — pass-rate TBD (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes: none

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-dsar-lifecycle.spec.ts — exports via DSAR workflow

Use cases referencing this story: none enumerated

SME notes:

  • GDPR Art. 20 + DPDPA §11 require "structured, commonly used and machine-readable" format — JSON satisfies but is not the only legal format. Some EU regulators prefer CSV-per-table; some prefer JSON-LD.
  • The export is single-tenant: cross-property stays for a guest spanning multiple properties under the same tenant ARE included (per exportGuestData() description); cross-TENANT (rare) is not.
  • No async-export pathway — a guest with 200+ stays + 1000+ communications times out HTTP. Should be queued and emailed.
  • Story Scenario 2 says "internalNotes and vipLevel are excluded" — verify this in exportGuestData() (likely NOT excluded today; review file does not call out the exclusion).
  • The export bundles data that lives in OTHER services (reservations from reservation-service, folios from frontdesk-service) — guest-service relies on read-model-sync projections. If those projections lag, the export is incomplete. No SLO for projection lag.

Drift findings:

D-012 — [P1] [Story-incomplete + scale-gap] Sync HTTP-only export breaks for high-volume guests (>200 stays). DPDPA §11 requires response within 30 days — but a request that times out repeatedly is effectively unprocessed.

  • [SME-PROPOSED] Add AC: "Export endpoint SHALL be async-default: POST /gdpr/guests/:id/export enqueues a job, returns {exportId, status='QUEUED'}. Worker assembles the JSON, uploads to S3 with KMS encryption + presigned-URL retrieval, emails the link to the guest's contact email. Sync GET variant remains for guests with < 50 stays + < 100 communications (size-precheck gates it)."
  • [SME-PROPOSED] Add AC: "Export JSON SHALL conform to a versioned schema gdpr-export-v1.json published in @hmp/api-contracts. Fields excluded from export SHALL be enumerated: internalNotes, vipLevel, alerts, companyId (substitute company name only), staff actor IDs."
  • [SME-PROPOSED] Add AC: "Export SHALL include a data_provenance section listing source services + projection timestamps so the guest can verify completeness."

Catalog status: Partial (P1, 3 points) [catalog line 119] Verified status: Partial — basic record/history exists; richer consent model (purposes/version/expiry) gaps

Code locations:

  • hmp-guest-service/src/consent/consent.controller.ts:19-37 — POST /consent (record action), GET /consent/:guestId (history)
  • hmp-guest-service/src/consent/consent.service.ts — service implementation
  • hmp-guest-service/src/consent/dto/consent.dto.ts — RecordConsentDto
  • hmp-guest-service/prisma/schema.prisma — TWO consent models per CLAUDE.md:
    • ConsentLog (audit trail; consentType MARKETING/DATA_PROCESSING/COOKIE/COMMUNICATION, action GRANTED/REVOKED, ipAddress)
    • ConsentRecord (current state; purpose MARKETING/ANALYTICS/THIRD_PARTY_SHARING/PROFILING/LOYALTY/COMMUNICATION, granted, consentTextVersion)
  • Guest.marketingConsent + Guest.dataConsent booleans (third place where consent state lives — see D-007)

Unit + integration tests:

  • hmp-guest-service/src/consent/consent.service.spec.ts — record + history

Flow tests: NONE FOUND for consent

  • hmp-infrastructure/testing/flow-tests/src/flows/dpdpa-children-consent.flow.spec.ts — guardian-consent flow (US-004-001) — pass-rate TBD; partial overlap with consent management

Real-backend smokes: none

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/crm-guardian-fields.spec.ts — guardian-consent UI

Use cases referencing this story: none enumerated

SME notes:

  • Catalog rightly says "Partial". Two consent tables exist (ConsentLog + ConsentRecord) plus the Guest-level booleans, but the controller only exposes a single record/history pair. Operators cannot easily see "what is the current consent matrix for this guest across all purposes".
  • consentTextVersion field on ConsentRecord indicates the platform tracks WHICH version of the consent text the guest agreed to (DPDPA §6(1) requirement — guest must be informed of purpose). However there is no consent_text_versions table holding the actual text — operator cannot reproduce what the guest agreed to when audited.
  • No consent EXPIRY semantics. DPDPA implies consent is revocable at any time but does not auto-expire; UIDAI Aadhaar usage has 6-month freshness windows in some flows. Story silent.
  • POST /consent requires FRONT_DESK+ role — guest self-service revoke from a confirmation email link is impossible without an authenticated session. DPDPA §6(5) requires ease of withdrawal.

Drift findings: (deferred to D-007 + D-011 + new ones below)

  • [SME-PROPOSED] Add AC: "A consent_text_versions table SHALL hold the canonical text the guest agreed to (locale-aware: en/hi/ta/te/vi etc.). ConsentRecord.consentTextVersion SHALL be a FK to this table. Audit export SHALL render the exact text for the version + locale at the consent timestamp."
  • [SME-PROPOSED] Add AC: "Guest-facing revoke flow SHALL be available without staff intervention: a signed-URL endpoint POST /public/consent/revoke?token=<jwt> reachable via every marketing email's footer. JWT carries {guestId, purpose, exp} with 30-day expiry. Endpoint emits consent.revoked event."
  • [SME-PROPOSED] Add AC: "GET /consent/:guestId/current SHALL return the consent MATRIX (purpose × granted/revoked × consentTextVersion × at) so the operator sees the full state at a glance, not a chronological log."

US-004-013 — Guest Document Upload (Encrypted Storage)​

Catalog status: Partial (P1, 5 points) [catalog line 120] Verified status: Partial — presigned-URL works; SSE-KMS bucket-encryption verification gap remains (per EP-004 gap analysis)

Code locations:

  • hmp-guest-service/src/uploads/uploads.controller.ts:10-22 — POST /uploads/presigned-url
  • hmp-guest-service/src/uploads/uploads.service.ts — generatePresignedUrl(fileName, contentType, folder='guest-documents')
  • hmp-guest-service/src/guests/guests.service.ts:addIdentity() — accepts documentUrl (S3 path) on identity write
  • hmp-infrastructure/terraform/modules/storage/... — S3 bucket definitions ([CITED-BUT-NOT-FOUND] for aws_s3_bucket_server_side_encryption_configuration referencing SSE-KMS specifically for guest-documents prefix)
  • AWS S3 (per AP-T02-002 / MinIO sidecar in dev): aws_s3_bucket_cors_configuration.assets is enabled; KMS config per-prefix unverified

Unit + integration tests: NONE FOUND for uploads service specifically (no uploads.service.spec.ts)

Flow tests: NONE FOUND

Real-backend smokes: none

E2E (Playwright): NONE FOUND for upload widget on guest documents tab

Use cases referencing this story: none enumerated

SME notes:

  • EP-004 Gap Analysis explicitly flags this as "High impact — compliance risk" for Aadhaar storage per UIDAI guidelines requiring AES-256 at rest. The verification gap is the story.
  • No uploads.service.spec.ts exists. Presigned-URL generation has no automated test of: (a) bucket name correctness, (b) content-type allowlist, (c) max file size enforcement (current default in @hmp/storage = ?), (d) KMS key ID injection.
  • Controller has NO @Roles decorator — any authenticated user can mint a presigned PUT URL into guest-documents. Front-desk staff could overwrite an existing Aadhaar scan. RBAC gate is missing.
  • File-naming collisions: fileName is operator-provided; nothing prevents two operators from POSTing the same passport.jpg filename and overwriting each other.
  • UIDAI requires Aadhaar scans to be encrypted at rest and access-logged at the bucket level (CloudTrail S3 data events). EP-004 gap analysis says this is not verified.

Drift findings: (D-013 implied by gap analysis; carries from EP-004 §Gap Analysis above)

  • [SME-PROPOSED] Add AC: "S3 bucket holding guest-documents/ SHALL have aws_s3_bucket_server_side_encryption_configuration set to aws:kms with per-env CMK alias keynova-<env>/guest-documents. Terraform module storage SHALL enforce this; CI gate scans the rendered plan for sse_algorithm == 'AES256' and fails."
  • [SME-PROPOSED] Add AC: "S3 data-event logging SHALL be enabled on the guest-documents/ prefix via CloudTrail. Trail name keynova-<env>-guest-docs-data-events; logs land in keynova-cloudtrail-mgmt bucket (Plan 0 Object Lock 7y)."
  • [SME-PROPOSED] Add AC: "POST /uploads/presigned-url SHALL be RBAC-gated: roles SUPER_ADMIN, ADMIN, MANAGER, FRONT_DESK. Operator-supplied fileName SHALL be hashed (<sha256(fileName + uuid())>.<ext>) before being embedded in the S3 key to prevent collisions + path-traversal."
  • [SME-PROPOSED] Add AC: "Presigned PUT URL SHALL include Content-MD5 requirement (S3 enforces integrity); max file size 10 MB enforced via content-length-range condition; allowed content-types image/jpeg|image/png|application/pdf only."
  • [SME-PROPOSED] Author a new uploads.service.spec.ts covering: bucket name from env, content-type allowlist, key sanitization, KMS ID injection, MD5 condition. ≥8 specs.

Cross-cutting observations (not story-specific)​

These surface only when reading the EPIC end-to-end and don't fit cleanly in a single story section. They feed BF-013 (DSAR / GDPR / DPDPA Erasure) when it's reconciled.

  • No flow test for the /dpdp/* endpoints. DpdpService is exercised only by unit-level + (mocked) e2e (crm-dsar-lifecycle.spec.ts). The full cross-service path (submit DSAR → 30-day SLA timer → guest-erased cascade → ESC/POS receipt invalidation) is unverified end-to-end.
  • Internal vs Public consent revoke: today POST /consent is staff-only. DPDPA §6(5) "ease of withdrawal" is structurally hard to satisfy without a public revoke endpoint (D-007 / US-004-012 SME-PROPOSED above).
  • AI-summary endpoint (/guests/:id/ai-summary) lives on GuestsController per CLAUDE.md but is not referenced by any catalog story. P2 orphaned-code candidate; defer to Day-2 catalog grooming.
  • Travel-agents controller (/travel-agents) reuses the Guest table with guestType='TRAVEL_AGENT' (Gotcha 5). Not a US-004 story but EPIC scope. Catalog rows for travel-agent management live under EP-010 (CRM).

See also​

  • ./PLAYBOOK.md — drift criteria, SME hat rules, sidecar pattern
  • ./EP-002-rtm.md — pilot RTM (this RTM matches its shape)
  • ../epics/EP-004-guest-management.md — live EPIC body (titles need realignment per D-001)
  • ../01-epic-catalog.md lines 108-123 — catalog (canonical title source per Wave 1 scoping)
  • ../reviews/EP-004/US-004-{001..006}-review.md — 6 stale per-story reviews (2026-03-07); Wave 2 should refresh
  • hmp-guest-service/CLAUDE.md — service ground truth (Gotchas 9-15 are guest-PII-specific)