BF-003 — Check-In (Indian + International + C-Form)
Business flow (cross-EPIC user journey). Wave 2 of the RTM Reconciliation Scale-Out — second business flow after the BF-001 pilot. Property #1 launch-blocker: flow-6 was 6/13 PASS at the 2026-05-16 demo-readiness snapshot; the gaps it surfaces (Aadhaar 12-digit guard, HK status propagation, nested formC validation, C-Form auto-generation) are all India-compliance defects that the launch hotel hits on day one.
Each step names (a) the user story that owns the requirement, (b) the primary code path that runs it, (c) the closest automated test that exercises it, and (d) pre/post-conditions and notes. Where the spec contains no matching catalog story, the step is tagged
[SME-PROPOSED]as a NEW story for the SME's intake — these are the cross-EPIC gaps the RTM scale-out is designed to surface.Spans: EP-005A (check-in/check-out), EP-004 (guest + identity capture), EP-006 (housekeeping room-status propagation). Single SOR drift IDs reference the per-EPIC RTMs (
EP-005A-rtm.md→D-005A-NNN,EP-004-rtm.md→D-NNN,EP-006-rtm.md→D-NNN).
Personas
| Persona | Role | Source |
|---|---|---|
| FRONT_DESK_AGENT | Receptionist staffing the arrivals desk | EP-005A US-005-001 |
| FRONT_DESK_MANAGER | Shift manager — authorises force-checkout, overrides, complimentary upgrades | EP-005A RoomMoveDto.managerAuthorizationCode |
| GUEST | Indian or foreign national arriving at the property | EP-004 + EP-005A |
| KIOSK_SERVICE | Kiosk software (self-check-in) | EP-005A US-005A-02 |
| BOOKING_ENGINE_SERVICE | Mobile pre-arrival / express check-in token holder | EP-005A US-005-012 |
Entry points
| Entry | URL | Backed by |
|---|---|---|
| Staff check-in (canonical) | POST /api/check-in/:reservationId | hmp-frontdesk-service/src/checkin/checkin.controller.ts:50 |
| Walk-in (no reservation) | POST /api/check-in/walk-in | …/checkin.controller.ts POST /walk-in |
| Batch / group check-in | POST /api/check-in/batch (≤ 50 rows) | …/checkin.controller.ts:53 |
| Express check-in (mobile pre-reg) | POST /api/check-in/express-check-in | …/checkin.controller.ts POST /express-check-in |
| Guest-portal pre-arrival | POST /api/guest-portal/checkin/:token | …/guest-checkin/ |
| Kiosk self-check-in | POST /api/self-checkin/start + …/submit | …/self-checkin/self-checkin.controller.ts |
| Dashboard wizard (staff UI) | https://dashboard.<env>.keynova.ai/front-desk/check-in/[id] | hmp-web-dashboard/src/app/(dashboard)/front-desk/check-in/[id]/page.tsx |
| Arrivals board (find reservation) | https://dashboard.<env>.keynova.ai/front-desk/arrivals | hmp-web-dashboard/src/app/(dashboard)/front-desk/arrivals/page.tsx |
The 12-step journey
Step 1: Reservation arrives in CONFIRMED state (pre-condition)
- Story: US-003-001 (Create Reservation — owned by EP-003; not in this BF's reconciliation scope) and US-005-006 (Arrivals / Departures / In-House Lists, EP-005A).
- Code: Reservation row is created upstream (booking-engine, channel-manager, walk-in, direct phone-in). State machine sits at
Reservation.status='CONFIRMED'witharrivalDate=<today>.Property.currentBusinessDate(AP-T09-004) gates the arrival-date guard inside the check-in service. - Test:
hmp-frontdesk-service/src/checkin/checkin.service.spec.ts:: "rejects PENDING reservation" + "rejects arrival in future" — PASS. - Pre-conditions: Guest has paid the deposit / authorised the card / qualifies as guaranteed reservation (channel-specific).
Property.currentBusinessDate == Reservation.arrivalDate(or override viaarrivalDateOverridein the DTO for staff back-date). - Post-conditions: Reservation visible on
GET /api/properties/:propertyId/arrivalswith summary block ({ total, checkedIn, pending, vip }). - Notes: The D1 stale-read-model reconciler (
reservation-status-reconciler.ts, Gotcha 28 inhmp-frontdesk-service/CLAUDE.md, shipped 2026-05-22) heals the case where the frontdesk local Reservation read-model is still atPENDINGwhile reservation-service canonical isCONFIRMED. It silently round-trips via the service-account JWT before the check-in service rejects the call.uc-staff-checkin-reservation-reconciler.yamlis[CITED-BUT-NOT-FOUND]— load-bearing code with no UC (see EP-005A D-005A-007).
Step 2: Front-desk agent searches the arrivals board
- Story: US-005-006 — Arrivals / Departures / In-House Lists (EP-005A).
- Code:
hmp-frontdesk-service/src/checkin/checkin.service.ts::getArrivalsBoard()returns{ arrivals, summary }. Dashboard hook:useArrivalsinhmp-web-dashboard/src/hooks/. Current implementation callsPOST /reservations/searchrather than the dedicatedGET /properties/:propertyId/arrivalsendpoint (per US-005-006 review GAP-8), losing the summary block. - Test:
hmp-frontdesk-service/src/checkin/checkin.service.spec.ts:: 3getArrivalsBoardspecs (summary counts, empty state, filter) — PASS. - Pre-conditions: Step 1 complete; agent logged in with
FRONT_DESK_AGENT(or higher) role;x-property-idJWT-claim scoped to the property. - Post-conditions: Arrivals list rendered with VIP badges, group badges (
Reservation.grouprelation — currently NOT included in the Prisma query → group-arrival badge missing per US-005-006 GAP-5), expected ETA, room-preferences notes. - Notes: Auto-refresh uses
staleTime: 60sbut story FR-9 requires 30 s interval (US-005-006 SME note).[SME-PROPOSED]follow-up —US-005-NEW-ARRIVALS-AUTO-REFRESH"Arrivals board polls every 30 s OR subscribes toreservation.checked_in/reservation.createdWebSocket events (mirrors D-005A-012 room-grid pattern)".
Step 3: Identity capture (6 doc types + format validators)
- Story: US-004-004 (Identity Document Management — 6 doc types: PASSPORT / AADHAAR / DRIVING_LICENSE / VOTER_ID / PAN_CARD / NATIONAL_ID) + US-005-015 (ID Scanner Integration).
- Code:
hmp-frontdesk-service/src/checkin/dto/checkin.dto.ts:142—CheckInDto.idDocumentType: IdDocumentTypeenum +idDocumentNumber: stringwith only@MinLength(4)+@MaxLength(30)at lines 147-148. Format-specific regex peridDocumentTypeis NOT enforced (see Step 4 — gap). Photo upload:hmp-frontdesk-service/src/checkin/id-photo-upload.controller.ts+.service.ts(S3 presigned-URL flow per AP-T06-005). Scanner hand-off (when hardware present):hmp-frontdesk-service/src/id-scanner/id-scanner.service.tsparses Aadhaar QR XML, Passport MRZ TD3, DL formats. Backend masking + Verhoeff:hmp-guest-service/src/guests/guests.service.ts::validateIdFormat()lines 105-122 +maskAadhaar()lines 44-100. - Test:
hmp-frontdesk-service/src/checkin/id-photo-upload.service.spec.ts,id-photo-upload-route-order.spec.ts.hmp-guest-service/src/guests/guests.service.spec.ts— Aadhaar Verhoeff + PAN regex + masking. Flow:flow-6-check-in.spec.ts:: S1 (Indian Aadhaar happy path 12-digit123456789012) — PASS. - Pre-conditions: Step 2 complete; physical ID (or scanner-captured payload) in hand. Guest is the named adult on the reservation OR a guardian per DPDPA §9 (US-004-001 Gotcha 9 — guardian fields enforced at create-time on guest-service, not check-in-time).
- Post-conditions:
CheckIn.idDocumentType+idDocumentNumberpersisted. If guest profile newly attached,GuestIdentityrow written via guest-service (POST /api/guests/:id/identities). Aadhaar values returned throughGET /api/guests/:idare masked toXXXX XXXX <last4>(DPDPA Act 2023 §8(4) + UIDAI guidelines per EP-004 D-006). Full-Aadhaar decrypt access (SUPER_ADMIN only) writes anAuditLogrow. - Notes:
idType=OTHERis removed from the 6-enum canon — foreign visa data lives in the dedicatedGuestVisamodel (AP-T06-002, see EP-004 D-006 second[SME-PROPOSED]bullet). Data-migration SQL:hmp-guest-service/prisma/migrations-data/migrate-guest-identity-visa.sql. The frontend "Documents" tab still uses a stale 4-type set (passport/id_card/driving_license/other) that does not include Aadhaar — drift surfaced in EP-004 review file.
Step 4: Aadhaar 12-digit validation (FINDING-F6-01 — gap!)
- Story:
[SME-PROPOSED]new story —US-005-NEW-ID-FORMAT-GUARDS"Per-document-type format validation in CheckInDto". No catalog row owns the per-type regex enforcement at the check-in surface. US-004-004 covers guest-service validation but the frontdesk DTO is the first line of defence at check-in and currently has none beyond@MinLength(4). - Code:
hmp-frontdesk-service/src/checkin/dto/checkin.dto.ts:147—@MinLength(4) @MaxLength(30) idDocumentNumber!: string;. Missing:@ValidateIf((o) => o.idDocumentType === 'AADHAAR') @Matches(/^\d{12}$/). A 5-digit "12345" Aadhaar is silently accepted → check-in returns 201. The downstream feedreporting-feeds/frontdesk/police-registerthen carries an invalid Aadhaar — a P0 compliance defect under Aadhaar Act 2016 + UIDAI Circular. - Test:
hmp-infrastructure/testing/flow-tests/src/flows/flow-6-check-in.spec.ts:: S2 "check-in with Aadhaar < 12 chars returns 400 — FINDING-F6-01" — currently FAIL by design until the validator ships (S2 hard-asserts 4xx and only passes today because the reservation is alreadyCHECKED_INso the state guard returns 409 — a coincidental 4xx that masks the missing format guard). EP-005A drift ID D-005A-003 (P0). - Pre-conditions: Step 3 entered an
idDocumentNumber. - Post-conditions (desired): 400 with code
AADHAAR_INVALID_FORMATwhen Aadhaar regex fails. PAN:/^[A-Z]{5}[0-9]{4}[A-Z]$/. VOTER_ID:/^[A-Z]{3}[0-9]{7}$/. DRIVING_LICENSE:/^[A-Z]{2}[0-9]{2} ?[0-9]{11}$/. PASSPORT (India):/^[A-Z][0-9]{7}$/(relaxed whenissueCountry !== 'IN'). All return 400 with<TYPE>_INVALID_FORMATcodes — see D-005A-003 SME-proposed ACs. - Notes: P0 launch-blocker. The Aadhaar Verhoeff checksum lives in guest-service (
guests.service.ts::validateIdFormat()) but is NOT triggered by the check-in path because the check-in only writesCheckIn.idDocumentNumberdirectly to the frontdesk-service Prisma row — the guest-service code path is bypassed unless the dashboard also callsPOST /api/guests/:id/identities(which today it does NOT, post-AP-T06-005). The format guard MUST live at the frontdesk DTO layer. UIDAI test numbers234123412346or999941057058are mandatory for any test author wiring the Verhoeff path (see EP-004 Gotcha 15).
Step 5: C-Form (FRRO Form C) for international guests with passport + visa
- Story: US-005-001 "Standard Check-In" + AP-T06-002 Form-C/visa data model + Foreigners Act 1946 §3 (FRRO regulation, Gotcha 13 in
hmp-frontdesk-service/CLAUDE.md). The catalog row for C-Form generation as a first-class user story is[CITED-BUT-NOT-FOUND]—[SME-PROPOSED]new storyUS-005-NEW-FORM-C"FRRO Form C generation at international-guest check-in". - Code:
hmp-frontdesk-service/src/checkin/dto/checkin.dto.ts:62—FormCDto { nationality, passportNo, passportPlaceOfIssue, passportIssueDate, passportExpiryDate, visaNo, visaType, visaPlaceOfIssue, visaIssueDate, visaExpiryDate, arrivedFrom, addressInIndia, stateInIndia, cityInIndia }. Wired intoCheckInDto.formC?: FormCDtoat line 128. C-Form generator:hmp-frontdesk-service/src/cform/—CFormService.generateCFormcalled inside the check-in transaction whenformCis present, persists aCFormrow with passport + visa fields encrypted. List endpoint:GET /api/frontdesk/cform?propertyId=…&nationality=…(cform.controller.ts); detail:GET /:id/pdf+POST /:id/mark-submitted. List response is hydrated viaGuestServiceClientper F-08 (2026-04-18). PlainGET /:iddetail endpoint is NOT shipped (percform-foreigner-reg.flow.spec.tsScenario 2 soft-skip). - Test:
flow-6-check-in.spec.ts:: S3 "International guest check-in with passport + C-Form generation" — PASS for happy path on a freshintlReservation; S4 "PASSPORT but missingformC.passportNo" — currently FAIL by design until FINDING-F6-04 (FormCDto absent-key validation) ships.cform-foreigner-reg.flow.spec.ts:: S1 generate + S3 list + S4 PII-masking — TBD pass-rate. - Pre-conditions:
Reservation.guest.nationality !== 'IN'(or passport-bearing Indian dual national);idDocumentType=PASSPORT;formCpayload supplied. Visa-page scan or manual entry ofvisaNo/visaType/visaExpiryDate. - Post-conditions:
CFormrow persisted with all 14 FormCDto fields.Reservation.checkInDateset. Per state-of-India compliance: police-register feed (reporting-feeds/frontdesk/police-register) emits the row within 24 h viamark-submittedPOST (manual today; auto-submit to FRRO RAS portal is a Day-2 backlog item). - Notes: FINDING-F6-04 (nested-formC validation): when the
formCobject is sent WITHOUT apassportNokey entirely (vspassportNo: ""), class-validator's@IsNotEmpty()does not fire on absent keys. Fix requires@IsDefined()AND@IsNotEmpty()onFormCDto.passportNo. See EP-005A[SME-PROPOSED]follow-up to D-005A-003. Express check-in path DOES NOT enforce FRRO (Gotcha 13 — per design, express is Indian-national-only). The express endpoint MUST rejectReservation.guest.nationality !== 'IN'per[SME-PROPOSED]AC in US-005-012 SME notes.
Step 6: Folio creation (OPEN)
- Story: US-005-001 (Standard Check-In) + EP-005B folio ownership (out of scope of this BF; reconciled separately in EP-005B-rtm).
- Code:
hmp-frontdesk-service/src/checkin/checkin.service.ts::checkIn()calls the folio bootstrap inside the same transaction. CreatesFolio { reservationId, propertyId, status='OPEN', folioType=<INDIVIDUAL|COMPANY>, balance=0 }. Returns the folio reference in the check-in response. - Test:
checkin.service.spec.ts— folio bootstrap covered in the 20-spec suite. Flow:flow-6-check-in.spec.ts:: S5 "an OPEN folio exists for the Indian guest reservation (by-reservation lookup)" — PASS viaGET /api/folios/by-reservation/:id(mitigating FINDING-F6-03 — see Notes). - Pre-conditions: Steps 1-3 complete; tax configuration on the property is non-null (AP-T02-005 inclusive flag determines downstream room-charge tax computation).
- Post-conditions: One
Foliorow inOPENstate.Folio.companyId+Folio.buyerGstinpopulated if D-005A-005 [P1] AC is implemented (today: NOT, GSTIN has to be retrofitted at folio-settle / e-invoice time — a P1 India-hospitality friction point). - Notes: FINDING-F6-03 (folio ID from check-in response unreliable) —
flow-6flow test mitigates by usingGET /api/folios/by-reservation/:idexclusively. Dashboard hits the same race intermittently. EP-005A drift ID D-005A-006 (P1). Sponsor-billing variant (sponsored-billing.service.ts, Vishramgruh hospital-guest-house pattern) creates a SPLIT folio when the reservation issponsoredFolio=true— see EP-005A US-005-002 SME notes.
Step 7: Room assignment (status VACANT_CLEAN → OCCUPIED)
- Story: US-005-001 (room assignment) + US-002-003 (Room Inventory — owned by EP-002).
- Code:
hmp-frontdesk-service/src/checkin/preference-matcher.service.ts— auto-assigns aVACANT_CLEANroom matching guest preferences (high floor, smoking/non-smoking, accessible, connecting). Operator can override viaCheckInDto.roomId. ThecheckIn()orchestrator validates the target room isstatus='VACANT_CLEAN'(rejectsOCCUPIED,DIRTY,OUT_OF_ORDER,OUT_OF_SERVICE) and then dispatchesroomStatusService.updateRoomStatus(roomId, 'OCCUPIED')via property-service.RoomReservation.roomIdis also written so the tape chart (/front-desk/calendar) starts showing the assignment immediately (seeseed-tape-chart-room-assignments.shfor the seed-time companion). - Test:
checkin.service.spec.ts:: "occupied/dirty room rejection", "VACANT_CLEAN auto-assign" — PASS. Flow:flow-6-check-in.spec.ts:: S5 "room status is OCCUPIED after check-in" — PASS (pollsGET /api/rooms/:idforstatus='OCCUPIED'orisOccupied=true). - Pre-conditions: Step 6 complete; at least one
VACANT_CLEANroom of the right room-type exists; room belongs to the property (cross-tenant denial viaassertPropertyTenant). - Post-conditions:
Room.status='OCCUPIED'in property-service.Room.isOccupied=true(alternative boolean for legacy clients).RoomReservation.roomIdset. Property-service emitsROOM_STATUS_CHANGEDKafka event. - Notes: Cross-tenant denial spec:
hmp-frontdesk-service/src/checkin/__tests__/gsa-010-011-property-required.spec.ts— PASS. Hard-block: even withforceCheckoutor any other override flag, anOCCUPIEDtarget room cannot be re-assigned without an explicitRoomOpsService.roomMoveround-trip (Gotcha 29, AP-T05-005/006 — the canonical room-move impl is inroom-ops.service.ts).
Step 8: HK status propagation (FINDING-F6-02 — gap!)
- Story:
[SME-PROPOSED]new story —US-006-NEW-HK-STATUS-CASCADE"Housekeeping status auto-transitions on check-in". No catalog row owns the cross-service handoff that flipsRoom.housekeepingStatustoOCCUPIED_NEEDS_SERVICE(or equivalent) when a guest checks in. Today the check-in cascade flipsRoom.status(front-office lane in property-service) but does not flipRoom.housekeepingStatusin the housekeeping-service Prisma replica — the HK board's "occupied & needing service" filter stays empty. - Code:
hmp-frontdesk-service/src/checkin/checkin.service.ts:685publishesRESERVATION_CHECKED_INKafka event. Housekeeping-service consumes it but the consumer does not writeRoom.housekeepingStatus— only the room-status lane on property-service moves.hmp-housekeeping-service/src/rooms/rooms.service.ts::getDiscrepancies()will surface this asfrontOfficeStatus=OCCUPIED, housekeepingStatus=CLEANdiscrepancy (NEEDS_ATTENTION), exposing the gap operationally. - Test:
flow-6-check-in.spec.ts:: S5 "room housekeeping status is DIRTY after check-in (or warns if field absent)" — currently warn-only soft-fail by design. EP-005A drift ID D-005A-004 (P0). - Pre-conditions: Step 7 complete; HK service consuming the Kafka topic.
- Post-conditions (desired):
Room.housekeepingStatus='OCCUPIED_NEEDS_SERVICE'(canonical lane TBD by SME — current enum isCLEAN | DIRTY | INSPECTED | PICKUP; a new value or repurposedDIRTYmay suffice).room_occupancy_summarymaterialized view stays correct. - Notes: P0 launch-blocker — the housekeeping board is a daily-driver view and the dashboard's discrepancy filter (
getDiscrepanciesper EP-006 line 498) already classifies this state asPOSSIBLE_SKIP/NEEDS_ATTENTION, meaning every check-in creates a false-positive discrepancy. Fix path: HK-sideRESERVATION_CHECKED_INconsumer writesRoom.housekeepingStatus. Alternative: frontdesk emits a dedicatedroom.status.changedKafka event carrying both lane updates. D-005A-004 P0 + EP-006 [SME-PROPOSED] AC under US-006-001 covers the two-lane surfacing.
Step 9: Key card encoding (hardware integration)
- Story: US-005-013 (Keycard Management — Encode/Re-encode/Deactivate) + US-005-014 (Smart Lock Integration — Not Started for real BLE devices, Day-2 backlog).
- Code:
hmp-frontdesk-service/src/keycard/keycard.service.ts::encodeForCheckIn(roomId)— RFID/BLE adapter via@hmp/hardware. Non-blocking: retry-with-backoff per P1-A5 (Gotcha 17 — 3 attempts, 200/500 ms backoff). Returns{success, error?, attempts}so the dashboard surfaces "encoded on retry" warnings. Mock mode:HARDWARE_MOCK=true(dev compose only; prod/staging compose enforcesHARDWARE_MOCK=falseper Gotcha AP-T12-005). Smart-lock variant (smart-lock/module) is scaffold-only. - Test:
hmp-frontdesk-service/src/keycard/keycard.service.spec.ts:: retry-with-backoff specs — PASS.keycard.controller.spec.ts— controller assertions PASS. - Pre-conditions: Step 7 complete (room is assigned); hardware encoder reachable (or
HARDWARE_MOCK=truein dev). - Post-conditions:
KeyCardrow written withroomId,reservationId,encodedAt. Physical card programmed with door-lock data + guest stay window. Check-in is NOT rolled back on encoder failure — staff can re-encode viaPOST /keycards/reencodeseparately (operations practice: hand the guest a paper "encoding in progress" slip and call HK to bring a backup encoder). - Notes: Smart-lock real-device wiring (US-005-014) is
Not Startedper Catalog. Indian-market vendors include ASSA ABLOY Vingcard, dormakaba Saflok, Salto KS, Onity, ZKTeco — story body should be vendor-agnostic via the@hmp/hardwareSmartLockAdapterinterface (see EP-005A US-005-014[SME-PROPOSED]AC). Failure-mode F4 below covers encoder-offline scenarios.
Step 10: Registration card print + signature
- Story: US-005-011 (Registration Card Generation — PDF) + state-specific compliance (
[SME-PROPOSED]D-005A-016). - Code:
hmp-frontdesk-service/src/grc/grc.controller.ts—POST /grcgenerates the registration card PDF. Service:grc/grc.service.ts. Signature capture: tablet/kiosk → PNG base64 OR printed-and-scanned upload →GuestIdVerification.signatureUrl(S3 presigned-URL flow per AP-T06-005). Per-state template selector: NOT shipped today — one-size-fits-all template. - Test:
grc.service.spec.ts— PASS. No dedicated flow-test today. - Pre-conditions: Steps 3-9 complete (guest, ID, room, key card all known); guest physically present to sign (or kiosk-touch signature for US-005A-02 self-check-in path).
- Post-conditions: PDF generated with: full name, age, sex, nationality, profession, address, purpose of visit, accompanying persons (with relationship), date+time of arrival, expected departure, last/next residence, signature panel, buyer GSTIN (when D-005A-005 [P1] implemented), property GSTIN, room number, rate, total estimated tariff. Stored as artifact + emailed to guest (when consent flag set per US-004-012).
- Notes: State-specific mandatory fields (D-005A-016 [P1] [India-compliance]):
- MH (Maharashtra): Aadhaar last-4 + relation to head-of-household
- KA (Karnataka): Karnataka Hotels & Lodging Houses regulations baseline
- TN (Tamil Nadu): police-permit-number for foreign guests
- DL (Delhi): standard national template
- GA (Goa): tourist-permit-number where applicable
- Other states: national-generic fallback with audit-log warning
Many states require police-register submission within 24 h — feed lives at
reporting-feeds/frontdesk/police-register. Submission to the actual police portal is manual today (operator downloads + uploads) — automation is a Day-2 backlog item.
Step 11: Reservation status → CHECKED_IN
- Story: US-005-001 (Standard Check-In) — final state transition.
- Code:
checkin.service.ts::checkIn()atomically (a) writesReservation.status='CHECKED_IN'on the frontdesk local read-model, (b) mirrors to reservation-service canonical viaReservationServiceClient, (c) publishesRESERVATION_CHECKED_INKafka event on the message bus (line 685). Downstream consumers (CRM loyalty earn, analytics MVs, notification welcome dispatch) read the event. - Test:
flow-6-check-in.spec.ts:: S1 "reservation status propagates to CHECKED_IN" (pollsGET /api/reservations/:idup to 30 s) — PASS for the Indian guest path; S3 equivalent for international path also PASS onceintlReservationis fresh. - Pre-conditions: Steps 6-10 all complete (folio open, room occupied, key card encoded — encoder failure is non-blocking per Step 9, so check-in proceeds with a flag).
- Post-conditions:
Reservation.status='CHECKED_IN'in both frontdesk-service and reservation-service.RESERVATION_CHECKED_INevent delivered to: (a)hmp-analytics-service(folio_revenue_summary, daily_occupancy MV refresh), (b)hmp-crm-service(loyalty earn — per-reservation dedup required, see EP-005A D-005A-001 [P1] for the batch-check-in variant), (c)hmp-notification-service(welcome message — see Step 12), (d)hmp-channel-service(mirror status back to OTA), (e)hmp-housekeeping-service(HK status cascade — see Step 8 gap). - Notes: The mirror to reservation-service canonical can race the check-in response; the D1 reconciler (Step 1 Notes) closes the inverse direction (canonical → local). The forward direction (local → canonical) is handled in the
ReservationServiceClientcall inside the same transaction — failure rolls back the check-in. Walk-in saga (Reservation.walkInRecoveryState, Gotcha 16) provides recovery for the 4 side-effects when the in-transaction publish fails: room status, reservation-service mirror, event publish, ID verification persist — retry endpointPOST /walk-in/:id/recover.
Step 12: Welcome message dispatch
- Story:
[SME-PROPOSED]new story —US-005-NEW-WELCOME-DISPATCH"Welcome message dispatch on check-in". No catalog row owns the welcome-message templating + multi-channel dispatch at check-in time. EP-013 (CRM) covers per-guest comms preferences but not the trigger. - Code:
hmp-notification-serviceconsumesRESERVATION_CHECKED_INand dispatches via the templating engine (@hmp/template-engineblock-tree → MJML → HTML; channel renderers for SMS/WhatsApp/push/in-app). Default templates: 5-channel__fallbackstrings in@hmp/notification-contractsv1 events (per shared-packages CLAUDE.md). Tenant per-property overrides via the visual editor (Workstream A). Consent matrix: enforcesGuestCommunicationPreference{channel, category=TRANSACTIONAL, optedIn=true}— DPDPA + TRAI DND gates. - Test: No dedicated check-in welcome-dispatch flow test today. Notification-service unit tests cover the template-rendering path. EP-004 D-007 (P0) tracks the THREE places consent state lives drift (Guest booleans + GuestCommunicationPreference + ConsentLog/ConsentRecord) — single-source-of-truth fix is required before launch.
- Pre-conditions: Step 11 complete;
Guest.emailorGuest.phonenon-null; consent gate cleared for at least one channel. - Post-conditions: Welcome message queued/dispatched to: (a) email with WiFi credentials, breakfast timing, checkout time, hotel facilities; (b) SMS with room number + WiFi password (TRANSACTIONAL category, no DND gate); (c) WhatsApp message with hotel concierge contact (if WhatsApp opt-in). Dispatch log written to
CommunicationLog(US-004-008 — Partial; EP-004 D-011 P1 tracks thecommunication.dispatchedevent no service emits to guest-service). - Notes: Marketing-categorized messages (PROMOTIONAL — upsell, loyalty enrolment offer) MUST gate on
GuestCommunicationPreference{category=PROMOTIONAL, optedIn=true}— TRAI DND violation otherwise. EP-004 D-007 (P0) — fix is to makeGuest.marketingConsenta DERIVED column from the per-channel × per-category matrix.[SME-PROPOSED]new storyUS-013-NEW-CHECKIN-WELCOME-TEMPLATE"First-class welcome-message template with property-personalisation tokens (property name, room number, WiFi SSID/password, breakfast hours, concierge contact, checkout time, IST clock)".
Failure modes worth testing
| # | Scenario | Expected behaviour | Coverage today | Owning story / drift |
|---|---|---|---|---|
| F1 | Invalid Aadhaar format (5-digit 12345) | 400 with AADHAAR_INVALID_FORMAT | flow-6 S2 — FAIL by design (only 4xx because of pre-existing CHECKED_IN state, not because validator fires) | D-005A-003 [P0 launch-blocker] |
| F2 | International guest, no passport supplied (idDocumentType=PASSPORT, empty formC.passportNo key) | 400 with PASSPORT_REQUIRED / FORMC_PASSPORTNO_REQUIRED | flow-6 S4 — FAIL by design (FINDING-F6-04: class-validator absent-key bug) | D-005A-003 follow-up [P0] |
| F3 | HK status not flipped to DIRTY / OCCUPIED_NEEDS_SERVICE after check-in | Room.housekeepingStatus transitions in HK-service replica AND surfaces on GET /housekeeping/rooms/status | flow-6 S5 — soft-warn today | D-005A-004 [P0 launch-blocker] |
| F4 | Key card encoder offline at check-in | Check-in proceeds (non-blocking per P1-A5); dashboard surfaces "encoded on retry — please re-encode" warning; staff can POST /keycards/reencode later | keycard.service.spec.ts retry-with-backoff specs — PASS | US-005-013 |
| F5 | Target room not VACANT_CLEAN (OCCUPIED / DIRTY / OUT_OF_ORDER) | 400 with ROOM_NOT_AVAILABLE and reason code | checkin.service.spec.ts :: "occupied/dirty room rejection" — PASS | US-005-001 |
| F6 | Double check-in (reservation already CHECKED_IN) | 409 with RESERVATION_ALREADY_CHECKED_IN | Implicit in S2 — PASS (409 returned today) | US-005-001 |
| F7 | Walk-in without reservation (guest with no booking arrives) | POST /walk-in creates reservation + folio + room assignment + ID capture atomically; same-day duplicate detection (P1-A1) surfaces a confirmation prompt for fat-finger walk-ins | checkin.service.spec.ts walk-in path — PASS; saga recovery via POST /walk-in/:id/recover (P1-A2, Gotcha 16) | US-005-010 |
| F8 | Reservation in PENDING (not CONFIRMED) state | 400 with RESERVATION_NOT_CONFIRMED; D1 reconciler attempts heal first (canonical may be CONFIRMED while local is stale) | checkin.service.spec.ts :: "rejects PENDING reservation" — PASS; drift-defense.spec.ts reconciler — PASS | US-005-001 + D-005A-007 [P2] for UC gap |
| F9 | Arrival date in future (early check-in attempt before business date) | 400 with EARLY_CHECKIN_NOT_PERMITTED UNLESS arrivalDateOverride supplied by staff AND early-late-charge.service.ts calculates fee | checkin.service.spec.ts :: "rejects arrival in future" — PASS; early-late-charge.service.spec.ts — PASS (post-AP-T05-001) | US-005-008 + D-005A-014 [P1] for stale review |
| F10 | Express check-in for foreign guest (FRRO gap) | 400 with EXPRESS_CHECKIN_INDIAN_NATIONALS_ONLY; staff path required to enforce FRRO Form C + visa expiry | Not implemented today (gap) | US-005-012 [SME-PROPOSED] AC |
| F11 | Aadhaar UID stored in full (no masking) on guest profile | DPDPA Act 2023 §8(4) violation — full-decrypt access without AuditLog row written | Verify in Task 6 — id-scanner.service.ts Aadhaar QR XML parser path | EP-004 D-006 [P0 launch-blocker] + US-005-015 [SME-PROPOSED] verification |
| F12 | Buyer GSTIN supplied with state-prefix mismatching property's state | 400 with BUYER_GSTIN_STATE_MISMATCH (if D-005A-009 [P0] implemented) — drives CGST/SGST vs IGST split at folio settlement | Not implemented today (gap) | D-005A-005 [P1] + D-005A-009 [P0] |
| F13 | C-Form list endpoint 404 (gateway two-file rule miss) | Gateway returns 200; list includes hydrated guest names + nationality + passport (encrypted blob, not raw) | flow-6 S3 + cform-foreigner-reg.flow.spec.ts S3 — PASS | US-005-NEW-FORM-C [SME-PROPOSED] |
Day-2 onboarding (first 30 days, NOT in BF-003 scope)
Items the hotel typically tackles in the first 30 days post go-live for the check-in surface. Not in BF-003 scope but flagged so adjacent BFs know where to pick up:
- Smart-lock real-device wiring (US-005-014) — vendor-specific BLE driver for ASSA ABLOY Vingcard / dormakaba Saflok / Salto KS / Onity / ZKTeco. Day-2.
- FRRO RAS portal auto-submission — today the police-register feed is downloaded + manually uploaded; automation is a Day-2 backlog item.
- Express check-in mobile pre-arrival link (US-005-012 SME [SME-PROPOSED] AC) — booking-engine confirmation email embeds tokenised
/guest-portal/checkin/:token. - State-specific registration-card templates (D-005A-016) — MH/KA/TN/DL/GA + national fallback.
- Buyer GSTIN at check-in for corporate guests (D-005A-005 [P1]) — extend
CheckInDtowithbuyerGstin+companyId, set on bootstrap folio. - Manager-PIN gate for force-checkout (D-005A-010 [P1]) — owned by BF-005.
- Welcome-template editor (Workstream A) — visual block-tree editor for per-property welcome message branding.
- Consent matrix UI (US-004-012 SME [SME-PROPOSED]) —
GET /consent/:guestId/currentreturns purpose × granted/revoked × consentTextVersion matrix. - Public-consent revoke link (US-004-012 SME [SME-PROPOSED]) — signed-URL endpoint in marketing-email footer for DPDPA §6(5) ease-of-withdrawal.
- Aadhaar-scan SSE-KMS verification on
guest-documentsS3 bucket (US-004-013 EP-004 narrative) — verify CloudTrail S3 data events are logged. - Express auto-checkout night-audit hook (D-005A-008 follow-up) — owned by BF-006 night audit.
- Booking-engine guest-portal pre-arrival ID upload — kiosk parity, photo + signature pre-captured before arrival.
- Group / corporate-block check-in UI flow (US-005A-01) — current code accepts 50-row batch; UI is staff-spreadsheet today.
Drift summary
Cross-EPIC drift findings surfaced by BF-003, with severities. P0 = blocks Property #1 launch; P1 = ships next sprint; P2 = backlog.
| ID | Source | Severity | Description | Surfaced by step |
|---|---|---|---|---|
| D-005A-003 | EP-005A | P0 (launch-blocker) | FINDING-F6-01 — Aadhaar 12-digit format guard missing in CheckInDto. 5-digit Aadhaar silently accepted → check-in 201. Aadhaar Act 2016 + UIDAI Circular violation; downstream police-register feed carries invalid IDs. | Step 4 |
| D-005A-004 | EP-005A | P0 (launch-blocker) | FINDING-F6-02 — Room housekeepingStatus does NOT transition on check-in. Property-side Room.status flips to OCCUPIED but HK-side replica stays at CLEAN. Breaks EP-006 housekeeping board + creates false-positive discrepancies via getDiscrepancies(). | Step 8 |
| D-005A-006 | EP-005A | P1 | FINDING-F6-03 — Check-in response sometimes returns a folio.id not yet visible to GET /folios/:id. Dashboard + tests mitigate via GET /folios/by-reservation/:id. | Step 6 |
| D-005A-005 | EP-005A | P1 | Buyer GSTIN cannot be captured at check-in for corporate guests. Today: retrofitted at folio-settle / e-invoice time. Forces manual re-keying of registration card; risks GSTIN-state-prefix mismatch with CGST/SGST/IGST split (D-005A-009 P0 at folio settle). | Steps 6 + 10 |
| D-005A-007 | EP-005A | P2 | D1 stale-read-model reconciler (reservation-status-reconciler.ts, 2026-05-22 SEV-2 fix) is load-bearing but has NO story or UC. New engineers don't know it exists. | Step 1 |
| D-005A-016 | EP-005A | P1 | Registration card PDF does not branch by property state. MH/KA/TN/DL/GA mandates different fields; today one-size-fits-all template. | Step 10 |
| D-005A-001 | EP-005A | P1 | Catalog vs EPIC vs code drift — batch check-in (POST /check-in/batch) is shipped + has a spec but catalog row + EPIC body both say "Not Started". | Step 1 (entry point) |
| D-005A-002 | EP-005A | P1 | Catalog has no row for US-005A-02 (Kiosk Self-Check-In) or US-005A-01 (Batch) or US-005A-03 (Express Checkout). Catalog + EPIC body using two numbering schemes. | Entry points table |
| D-006 | EP-004 | P0 (launch-blocker) | Aadhaar masking + SEC-PRIV-04 audit-log on full-decrypt access ARE shipped in guest-service but story body uses hedged language and does not REQUIRE the audit log. UIDAI + DPDPA §8(4) mandate access logging — regression risk masks-then-unmasks silently. Also: frontend Documents tab uses stale 4-type set excluding Aadhaar. | Steps 3 + 4 + F11 |
| D-007 | EP-004 | P0 (launch-blocker) | Consent state lives in THREE places (Guest booleans + GuestCommunicationPreference + ConsentLog/ConsentRecord). TRAI DND + DPDPA require a SINGLE source of truth. Welcome-dispatch in Step 12 reads which? — operationally ambiguous. | Step 12 |
| D-011 | EP-004 | P1 | No service emits communication.dispatched to guest-service; the comms tab + CommunicationLog row in Step 12 is systematically incomplete. CRM cannot demonstrate marketing-pressure compliance to DPDPA auditors. | Step 12 |
| EP-006 D-003 | EP-006 | P0 (launch-blocker) | Three copies of HousekeepingTask.taskType enum disagree (EPIC body / Prisma+service / dashboard filter). A frontend sending the EPIC value MAINTENANCE would Prisma-fail at runtime; sending CLEANING (no underscore) would 422. Adjacent to check-in via AP-T05-001 CHECKOUT_CLEANING auto-task dedup (events.service.spec.ts). | Step 8 (downstream consumer) |
| FINDING-F6-04 | EP-005A (new) | P0 (launch-blocker) | FormCDto.passportNo validation does not fire when the key is absent (only when empty string). class-validator + class-transformer absent-key bug. Fix: add @IsDefined() alongside @IsNotEmpty(). | Step 5 + F2 |
US-005-NEW-ID-FORMAT-GUARDS | BF-003 (new story) | P0 (launch-blocker) | Catalog gap — no US-NNN row owns the per-document-type format regex enforcement at the check-in surface. PAN, VOTER_ID, DRIVING_LICENSE, PASSPORT (India + non-India) all need typed guards mirroring D-005A-003 Aadhaar gap. | Step 4 |
US-005-NEW-FORM-C | BF-003 (new story) | P0 (launch-blocker) | Catalog gap — FRRO Form C generation at international-guest check-in has no first-class user story. Implementation shipped (CFormService, cform.controller.ts, AP-T06-002 data model) but un-catalogued. | Step 5 |
US-006-NEW-HK-STATUS-CASCADE | BF-003 (new story) | P0 (launch-blocker) | Catalog gap — housekeeping-status auto-transitions on check-in have no story owner. Today the cross-service handoff is implicit and broken (D-005A-004 P0). | Step 8 |
US-005-NEW-WELCOME-DISPATCH | BF-003 (new story) | P1 | Catalog gap — welcome-message dispatch on check-in has no first-class user story. EP-013 (CRM) covers guest comms preferences but not the trigger. | Step 12 |
US-013-NEW-CHECKIN-WELCOME-TEMPLATE | BF-003 (new story) | P2 | Catalog gap — first-class welcome-message template with property-personalisation tokens (WiFi SSID/password, breakfast hours, concierge contact, IST clock). | Step 12 |
US-005-NEW-ARRIVALS-AUTO-REFRESH | BF-003 (new story) | P2 | Catalog gap — arrivals board auto-refresh interval drift (60 s today vs FR-9 30 s requirement); WebSocket subscription preferred. Mirrors D-005A-012 room-grid pattern. | Step 2 |
uc-staff-checkin-reservation-reconciler.yaml | BF-003 (UC proposal) | P2 | UC sidecar gap — D1 reconciler load-bearing code has no use-case file. New engineers cannot find the contract. | Step 1 Notes |
uc-walkin-saga-recovery.yaml | BF-003 (UC proposal) | P2 | UC sidecar gap — P1-A2 walk-in saga (Reservation.walkInRecoveryState + POST /walk-in/:id/recover) has no use-case file. | F7 |
P0 launch-blocker count surfaced by BF-003: 8 — D-005A-003, D-005A-004, D-006 (EP-004), D-007 (EP-004), EP-006 D-003, FINDING-F6-04, US-005-NEW-ID-FORMAT-GUARDS, US-005-NEW-FORM-C, US-006-NEW-HK-STATUS-CASCADE. Property #1 cannot pass UAT with any of these open — they all hit the India-compliance + cross-service-handoff seam at the very moment the guest stands at the front desk.
Cross-EPIC narrative summary
| Step | Spans | Why cross-EPIC |
|---|---|---|
| 1 → 2 | EP-005A | Arrivals board entry; D1 reconciler is the cross-service-with-no-UC seam |
| 3 → 4 | EP-005A ↔ EP-004 | Identity capture lives at EP-005A CheckInDto, format validation + masking live at EP-004 guest-service. Two enforcement points, neither fully owns the contract — the launch-blocker P0 gap. |
| 5 | EP-005A ↔ EP-004 | C-Form is owned by EP-005A but pulls passport + visa fields from EP-004 GuestVisa model (AP-T06-002). GuestServiceClient hydrates the list response (F-08). |
| 6 | EP-005A ↔ EP-005B (folios, out of scope) | Folio bootstrap inside the check-in transaction. Folio settlement at check-out is BF-005. |
| 7 → 8 | EP-005A ↔ EP-002 ↔ EP-006 | Room assignment writes to EP-002 property-service (Room.status); HK status MUST write to EP-006 housekeeping-service (Room.housekeepingStatus) — but does not today. The 2-EPIC handoff is the single biggest BF-003 finding (D-005A-004 P0). |
| 9 → 10 | EP-005A + @hmp/hardware | Hardware integration is non-blocking; check-in proceeds even when encoder is offline. |
| 11 | EP-005A ↔ EP-003 (reservations canonical) ↔ EP-013 (CRM loyalty) ↔ EP-012A (analytics) ↔ EP-009 (channel-manager) ↔ EP-006 (HK cascade) | RESERVATION_CHECKED_IN Kafka event fan-out is the cross-EPIC seam. 5 downstream consumers; one of them (EP-006 HK cascade) currently broken. |
| 12 | EP-013 (CRM) ↔ EP-004 (consent) | Welcome-message dispatch requires the consent matrix to be single-source-of-truth — today it's three sources (D-007 P0). |
The dominant BF-003 finding: Step 4 (Aadhaar guard) and Step 8 (HK status cascade) are the two India-compliance + cross-service-handoff defects that the Property #1 launch hotel will hit on day one. Both are P0. Both are flow-6 FINDINGs (F6-01, F6-02). Both have shipped fixes in the spec but missing implementations. The other 6 P0s (D-006, D-007, EP-006 D-003, FINDING-F6-04, US-005-NEW-ID-FORMAT-GUARDS, US-005-NEW-FORM-C, US-006-NEW-HK-STATUS-CASCADE) are catalog/story/contract gaps that the SME pass must close before BF-003 can be claimed launch-ready.