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.mdlines 108-123) assign DIFFERENT titles to the sameUS-004-NNNids from -002 onward. This RTM follows the catalog titles per Wave 1 task scoping. The drift is recorded asD-001.
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 (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-Built | 8 (US-004-001, -002, -003, -004, -005, -006, -009, -010) |
| Built-Untested | 1 (US-004-007 VIP — implicit via guest CRUD tests; no dedicated assertion) |
| Partial | 4 (US-004-008 Comm Log; US-004-011 GDPR Export; US-004-012 Consent; US-004-013 Doc Upload) |
| Stub | 0 |
| Not Started | 0 (all 13 in-scope stories have code) |
| Drift findings | 12 (D-001..D-012; 4 P0 / 6 P1 / 2 P2) |
| [SME-PROPOSED] refinements | 28 |
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/DataScopedecorators (Aadhaar in idNumber, etc.)hmp-guest-service/prisma/schema.prisma—Guestmodel with@@unique([propertyId, email]),vipLevel(0-5),guestType,totalStays/totalRevenue, plus DPDPA §9 fields (guardianName/guardianRelation/guardianPhone/guardianConsentAt) — see guest-service CLAUDE.md Gotcha 9hmp-web-dashboard/src/app/(dashboard)/crm/guests/page.tsx— guest list pagehmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx— guest detailhmp-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 maskinghmp-guest-service/src/guests/guests.controller.spec.ts— route declaration order pin (qa-PMS-9) + RBAC fan-outhmp-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 in2026-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 datahmp-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 + createhmp-web-dashboard/e2e/tests/crm-guest-detail.spec.ts— guest detail pagehmp-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/guardianPhoneif 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) —
validateIdFormatruns on everyAADHAARID number. Test-author hygiene mandates UIDAI test numbers234123412346or999941057058— NOT invented 12-digit strings. - AES-256-GCM encryption on
GuestIdentity.idNumberviaFieldEncryptionServiceis 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 reconciliationalias 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 tosrc/guests/guest-cross-tenant-denial.spec.ts(sibling) or add<rootDir>/src/**/__tests__/**/*.spec.tstojest.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 withsearchquery 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—SearchGuestsDtohmp-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 inputhmp-web-dashboard/src/app/(dashboard)/chain/guests/page.tsx— chain-level cross-property searchhmp-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 + indexinghmp-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 boxhmp-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-propertyandGET /guests/search-phoneMUST be declared BEFOREGET /guests/:id(which carriesParseUUIDPipe). The controller spec asserts this viaPATH_METADATAreflection — good defense. - There is no
GET /guests/searchroute — onlyPOST /guests/search. Plain typeahead usesGET /guests?search=<q>. This is a frontend integration trap not captured in the story. - Indian-hospitality phone-number normalization matters: a guest may search
9876543210while 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@IsEmailblocking 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/searchSHALL apply Jaro-Winkler similarity (threshold 0.85) onfirstName + lastNamewhenfuzzy=truequery param is set. Results SHALL include amatchScorefield. Exact-match results SHALL be returned first, fuzzy below them." - [SME-PROPOSED] Add AC: "Phone-number search SHALL normalize country-code variants: input
9876543210matches 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-propertySHALL be accessible toFRONT_DESKandRESERVATIONSroles 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/360hmp-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 identityhmp-guest-service/src/read-model-sync/read-model-sync.service.ts— inbound sync of Reservation/Folio/Property from event bus (per CLAUDE.mdReadModelSyncModule)hmp-web-dashboard/src/app/(dashboard)/crm/guests/[id]/page.tsx— guest detail page (usesuseGuest(id)+useGuestHistory(id), NOT the unified/360endpoint per review file — see D-005)
Unit + integration tests:
hmp-guest-service/src/guests/guests.service.spec.ts—get360View()aggregation paths + Aadhaar access audithmp-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/historyseparately 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/360as the primary fetch and render six tabs: Profile, Stays, Preferences, Identity, Communications, Loyalty.useGuest(id)SHALL be replaced byuseGuest360(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 checksumhmp-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 1234mask + SEC-PRIV-04 audit log on full-Aadhaar decrypt accesshmp-guest-service/src/guests/dto/guest.dto.ts:36-83—IdTypeenum (PASSPORT/AADHAAR/DRIVING_LICENSE/VOTER_ID/PAN_CARD/NATIONAL_ID/OTHER — 7 values; story says 6, the 7thOTHERwas the pre-AP-T06-002 hack for visas — see D-006),AddIdentityDtohmp-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_visashmp-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 + maskinghmp-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 usingGuestVisadata — 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 1234but 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 (
OTHERis the pre-AP-T06-002 hack used for visa data; now superseded by the dedicatedGuestVisamodel). 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
idNumberSHALL be returned in masked form (XXXX XXXX <last4>) for ALL roles exceptSUPER_ADMIN. SUPER_ADMIN full-decrypt access SHALL write anAuditLogrow (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 withcode=DUPLICATE_IDENTITY. Migration adds@@unique([guestId, idType, idNumber])toGuestIdentity." - [SME-PROPOSED] Add AC: "
idTypeenum SHALL be exactly the 6 catalog types (PASSPORT, AADHAAR, DRIVING_LICENSE, VOTER_ID, PAN_CARD, NATIONAL_ID).OTHERis REMOVED — foreign visa data SHALL live in the dedicatedGuestVisamodel (AP-T06-002, Gotcha 12). Data migrationmigrate-guest-identity-visa.sqlMUST 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-preferenceshmp-guest-service/src/preferences/preferences.service.ts— 5 categories: ROOM, FOOD, SERVICE, COMMUNICATION, ACCESSIBILITYhmp-guest-service/src/preferences/dto/preference.dto.ts— DTOs + PreferenceCategory enumhmp-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 pagehmp-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/commonper 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/dataConsentbooleans 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
GuestCommunicationPreferenceas the authoritative consent gate.Guest.marketingConsentSHALL 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 emitsconsent.conflict.detectedfor 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/preferencesper 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-previewhmp-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 Kafkaguest.mergedhmp-guest-service/src/guests/guest-merge.service.spec.ts— 20+ merge specshmp-guest-service/src/sync/deduplication.service.ts—findDuplicates()(cross-property) +getMergeSuggestions()(within-property) usingFuzzyMatchService(Jaro-Winkler)hmp-guest-service/src/sync/fuzzy-match.service.ts— Jaro-Winkler scorer@hmp/infraDistributedLockModule— 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 emithmp-guest-service/src/sync/guest-sync.service.spec.ts— dedup + fuzzy scoringhmp-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 Startedper catalog, butDeduplicationService.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 → Partialuntil the dashboard ships: (a) merge suggestions page at/crm/guests/duplicatesconsuminggetMergeSuggestions(), (b) preview-then-commit dialog calling/guests/merge-previewthen/guests/merge, (c) audit-trail link to the resultingguest.mergedKafka 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/undoendpoint 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.vipLevelinteger field (CLAUDE.md says 0-5; story says 0-3 — see D-010)hmp-guest-service/src/guests/dto/guest.dto.ts—vipLevelin CreateGuestDto/UpdateGuestDto with@IsInt() @Min(0) @Max(5)(matching schema)hmp-guest-service/src/guests/guests.service.ts— updates flow via standard PUT/guests/:idhmp-web-dashboard/src/app/(dashboard)/crm/guests/page.tsx— VIP badge columnhmp-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; checkhmp-web-dashboard/src/app/(dashboard)/frontdesk/arrivals)
Unit + integration tests:
- Implicit only — no dedicated
vipLevelspec inguests.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; updateGuest.vipLevelMax(N) and badge UI to match." - [SME-PROPOSED] Add AC: "Add a dedicated
vip-classification.spec.tsasserting: (a) updating vipLevel writesAuditLogrow; (b)Guest.alertstext 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.mdhmp-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 dedicatedCommunicationProjectionConsumer; 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 canonicalcommunication.dispatchedevent 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/printingESC/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.dispatchedevent 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-serviceSHALL publishcommunication.dispatchedevent on every send (EMAIL/SMS/WHATSAPP/PUSH) with payload{tenantId, propertyId, guestId, channel, direction='OUTBOUND', status, templateId, at}.hmp-guest-serviceSHALL consume via aCommunicationProjectionConsumerand upsert aGuestCommunicationrow." - [SME-PROPOSED] Add AC: "POS receipt printing (
hmp-pos-service) SHALL publishcommunication.dispatchedwithchannel='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—AddressTypeenum (HOME/WORK/BILLING/OTHER — 4 values; catalog story says 3),AddAddressDto,GuestAddressDtohmp-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 dedicatedaddAddressblock 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
Companyrow, not the Guest — the AC Scenario 1 conflates the two. Corporate billing addresses correctly belong toCompany.address(Company model in guest-service per CLAUDE.md). isPrimaryper 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}(dropOTHER) to match the catalog 3-types contract, OR extend the story to 4 types with explicitOTHERuse case. Choose one." - [SME-PROPOSED] Add AC: "
isPrimarySHALL 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 = BILLINGis set on a guest withcompanyId IS NOT NULL, the API SHALL warnBILLING_ADDRESS_SHOULD_BE_ON_COMPANYbecause GST e-invoices for corporates useCompany.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 recordshmp-guest-service/src/gdpr/gdpr.service.spec.ts— erasure spechmp-guest-service/src/gdpr/checkout-consumer.service.ts—CheckoutConsumerService(RESERVATION_CHECKED_OUT → createsGuestRetentionTimerwithexpiresAt = checkoutDate + 90d) — Gotcha 11hmp-guest-service/src/gdpr/retention-cron.service.ts—RetentionCronServiceruns at 02:10 daily; finds expired timers; auto-erases withlegalBasis='RETENTION_EXPIRY'(AP-T08-004) — Gotcha 11hmp-guest-service/src/gdpr/data-retention.service.ts— 3-year lastStay purge service- 8 service-side
guest-erased.consumer.tsconsumers (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.tsco-located (verified via Grep) hmp-shared-packages/packages/messaging/src/events/event-types.ts—guest.erasedevent type definitionhmp-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.tshmp-guest-service/src/gdpr/checkout-consumer.service.spec.tshmp-guest-service/src/gdpr/data-retention.service.spec.tshmp-guest-service/src/gdpr/retention-cron.service.spec.ts- 8 service-side
guest-erased.consumer.spec.tsfiles
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
GuestRetentionTimermodel +GdprErasureLogwithlegalBasisenum (RIGHT_TO_ERASUREfor manual /RETENTION_EXPIRYfor 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 reachesprocessedAt IS NOT NULLwithin 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-keyReservation.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-syncprojections. 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/exportenqueues 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. SyncGETvariant 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.jsonpublished 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_provenancesection listing source services + projection timestamps so the guest can verify completeness."
US-004-012 — Consent Management & Tracking
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 implementationhmp-guest-service/src/consent/dto/consent.dto.ts—RecordConsentDtohmp-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.dataConsentbooleans (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". consentTextVersionfield onConsentRecordindicates 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 noconsent_text_versionstable 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
/consentrequires 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_versionstable SHALL hold the canonical text the guest agreed to (locale-aware: en/hi/ta/te/vi etc.).ConsentRecord.consentTextVersionSHALL 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 emitsconsent.revokedevent." - [SME-PROPOSED] Add AC: "
GET /consent/:guestId/currentSHALL 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-urlhmp-guest-service/src/uploads/uploads.service.ts—generatePresignedUrl(fileName, contentType, folder='guest-documents')hmp-guest-service/src/guests/guests.service.ts:addIdentity()— acceptsdocumentUrl(S3 path) on identity writehmp-infrastructure/terraform/modules/storage/...— S3 bucket definitions ([CITED-BUT-NOT-FOUND]foraws_s3_bucket_server_side_encryption_configurationreferencing SSE-KMS specifically forguest-documentsprefix)- AWS S3 (per AP-T02-002 / MinIO sidecar in dev):
aws_s3_bucket_cors_configuration.assetsis 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.tsexists. 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
@Rolesdecorator — any authenticated user can mint a presigned PUT URL intoguest-documents. Front-desk staff could overwrite an existing Aadhaar scan. RBAC gate is missing. - File-naming collisions:
fileNameis operator-provided; nothing prevents two operators from POSTing the samepassport.jpgfilename 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 haveaws_s3_bucket_server_side_encryption_configurationset toaws:kmswith per-env CMK aliaskeynova-<env>/guest-documents. Terraform modulestorageSHALL enforce this; CI gate scans the rendered plan forsse_algorithm == 'AES256'and fails." - [SME-PROPOSED] Add AC: "S3 data-event logging SHALL be enabled on the
guest-documents/prefix via CloudTrail. Trail namekeynova-<env>-guest-docs-data-events; logs land inkeynova-cloudtrail-mgmtbucket (Plan 0 Object Lock 7y)." - [SME-PROPOSED] Add AC: "
POST /uploads/presigned-urlSHALL be RBAC-gated: rolesSUPER_ADMIN, ADMIN, MANAGER, FRONT_DESK. Operator-suppliedfileNameSHALL 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-MD5requirement (S3 enforces integrity); max file size 10 MB enforced viacontent-length-rangecondition; allowed content-typesimage/jpeg|image/png|application/pdfonly." - [SME-PROPOSED] Author a new
uploads.service.spec.tscovering: 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.DpdpServiceis 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 /consentis 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 onGuestsControllerper 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 withguestType='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.mdlines 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 refreshhmp-guest-service/CLAUDE.md— service ground truth (Gotchas 9-15 are guest-PII-specific)