Skip to main content

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​

PersonaRoleSource
FRONT_DESK_AGENTReceptionist staffing the arrivals deskEP-005A US-005-001
FRONT_DESK_MANAGERShift manager — authorises force-checkout, overrides, complimentary upgradesEP-005A RoomMoveDto.managerAuthorizationCode
GUESTIndian or foreign national arriving at the propertyEP-004 + EP-005A
KIOSK_SERVICEKiosk software (self-check-in)EP-005A US-005A-02
BOOKING_ENGINE_SERVICEMobile pre-arrival / express check-in token holderEP-005A US-005-012

Entry points​

EntryURLBacked by
Staff check-in (canonical)POST /api/check-in/:reservationIdhmp-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-inPOST /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-arrivalPOST /api/guest-portal/checkin/:token…/guest-checkin/
Kiosk self-check-inPOST /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/arrivalshmp-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' with arrivalDate=<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 via arrivalDateOverride in the DTO for staff back-date).
  • Post-conditions: Reservation visible on GET /api/properties/:propertyId/arrivals with summary block ({ total, checkedIn, pending, vip }).
  • Notes: The D1 stale-read-model reconciler (reservation-status-reconciler.ts, Gotcha 28 in hmp-frontdesk-service/CLAUDE.md, shipped 2026-05-22) heals the case where the frontdesk local Reservation read-model is still at PENDING while reservation-service canonical is CONFIRMED. It silently round-trips via the service-account JWT before the check-in service rejects the call. uc-staff-checkin-reservation-reconciler.yaml is [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: useArrivals in hmp-web-dashboard/src/hooks/. Current implementation calls POST /reservations/search rather than the dedicated GET /properties/:propertyId/arrivals endpoint (per US-005-006 review GAP-8), losing the summary block.
  • Test: hmp-frontdesk-service/src/checkin/checkin.service.spec.ts :: 3 getArrivalsBoard specs (summary counts, empty state, filter) — PASS.
  • Pre-conditions: Step 1 complete; agent logged in with FRONT_DESK_AGENT (or higher) role; x-property-id JWT-claim scoped to the property.
  • Post-conditions: Arrivals list rendered with VIP badges, group badges (Reservation.group relation — 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: 60s but 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 to reservation.checked_in / reservation.created WebSocket 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: IdDocumentType enum + idDocumentNumber: string with only @MinLength(4) + @MaxLength(30) at lines 147-148. Format-specific regex per idDocumentType is 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.ts parses 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-digit 123456789012) — 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 + idDocumentNumber persisted. If guest profile newly attached, GuestIdentity row written via guest-service (POST /api/guests/:id/identities). Aadhaar values returned through GET /api/guests/:id are masked to XXXX XXXX <last4> (DPDPA Act 2023 §8(4) + UIDAI guidelines per EP-004 D-006). Full-Aadhaar decrypt access (SUPER_ADMIN only) writes an AuditLog row.
  • Notes: idType=OTHER is removed from the 6-enum canon — foreign visa data lives in the dedicated GuestVisa model (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 feed reporting-feeds/frontdesk/police-register then 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 already CHECKED_IN so 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_FORMAT when 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 when issueCountry !== 'IN'). All return 400 with <TYPE>_INVALID_FORMAT codes — 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 writes CheckIn.idDocumentNumber directly to the frontdesk-service Prisma row — the guest-service code path is bypassed unless the dashboard also calls POST /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 numbers 234123412346 or 999941057058 are 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 story US-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 into CheckInDto.formC?: FormCDto at line 128. C-Form generator: hmp-frontdesk-service/src/cform/ — CFormService.generateCForm called inside the check-in transaction when formC is present, persists a CForm row 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 via GuestServiceClient per F-08 (2026-04-18). Plain GET /:id detail endpoint is NOT shipped (per cform-foreigner-reg.flow.spec.ts Scenario 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 fresh intlReservation; S4 "PASSPORT but missing formC.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; formC payload supplied. Visa-page scan or manual entry of visaNo/visaType/visaExpiryDate.
  • Post-conditions: CForm row persisted with all 14 FormCDto fields. Reservation.checkInDate set. Per state-of-India compliance: police-register feed (reporting-feeds/frontdesk/police-register) emits the row within 24 h via mark-submitted POST (manual today; auto-submit to FRRO RAS portal is a Day-2 backlog item).
  • Notes: FINDING-F6-04 (nested-formC validation): when the formC object is sent WITHOUT a passportNo key entirely (vs passportNo: ""), class-validator's @IsNotEmpty() does not fire on absent keys. Fix requires @IsDefined() AND @IsNotEmpty() on FormCDto.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 reject Reservation.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. Creates Folio { 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 via GET /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 Folio row in OPEN state. Folio.companyId + Folio.buyerGstin populated 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-6 flow test mitigates by using GET /api/folios/by-reservation/:id exclusively. 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 is sponsoredFolio=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 a VACANT_CLEAN room matching guest preferences (high floor, smoking/non-smoking, accessible, connecting). Operator can override via CheckInDto.roomId. The checkIn() orchestrator validates the target room is status='VACANT_CLEAN' (rejects OCCUPIED, DIRTY, OUT_OF_ORDER, OUT_OF_SERVICE) and then dispatches roomStatusService.updateRoomStatus(roomId, 'OCCUPIED') via property-service. RoomReservation.roomId is also written so the tape chart (/front-desk/calendar) starts showing the assignment immediately (see seed-tape-chart-room-assignments.sh for 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 (polls GET /api/rooms/:id for status='OCCUPIED' or isOccupied=true).
  • Pre-conditions: Step 6 complete; at least one VACANT_CLEAN room of the right room-type exists; room belongs to the property (cross-tenant denial via assertPropertyTenant).
  • Post-conditions: Room.status='OCCUPIED' in property-service. Room.isOccupied=true (alternative boolean for legacy clients). RoomReservation.roomId set. Property-service emits ROOM_STATUS_CHANGED Kafka event.
  • Notes: Cross-tenant denial spec: hmp-frontdesk-service/src/checkin/__tests__/gsa-010-011-property-required.spec.ts — PASS. Hard-block: even with forceCheckout or any other override flag, an OCCUPIED target room cannot be re-assigned without an explicit RoomOpsService.roomMove round-trip (Gotcha 29, AP-T05-005/006 — the canonical room-move impl is in room-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 flips Room.housekeepingStatus to OCCUPIED_NEEDS_SERVICE (or equivalent) when a guest checks in. Today the check-in cascade flips Room.status (front-office lane in property-service) but does not flip Room.housekeepingStatus in the housekeeping-service Prisma replica — the HK board's "occupied & needing service" filter stays empty.
  • Code: hmp-frontdesk-service/src/checkin/checkin.service.ts:685 publishes RESERVATION_CHECKED_IN Kafka event. Housekeeping-service consumes it but the consumer does not write Room.housekeepingStatus — only the room-status lane on property-service moves. hmp-housekeeping-service/src/rooms/rooms.service.ts::getDiscrepancies() will surface this as frontOfficeStatus=OCCUPIED, housekeepingStatus=CLEAN discrepancy (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 is CLEAN | DIRTY | INSPECTED | PICKUP; a new value or repurposed DIRTY may suffice). room_occupancy_summary materialized view stays correct.
  • Notes: P0 launch-blocker — the housekeeping board is a daily-driver view and the dashboard's discrepancy filter (getDiscrepancies per EP-006 line 498) already classifies this state as POSSIBLE_SKIP/NEEDS_ATTENTION, meaning every check-in creates a false-positive discrepancy. Fix path: HK-side RESERVATION_CHECKED_IN consumer writes Room.housekeepingStatus. Alternative: frontdesk emits a dedicated room.status.changed Kafka 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 enforces HARDWARE_MOCK=false per 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=true in dev).
  • Post-conditions: KeyCard row written with roomId, 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 via POST /keycards/reencode separately (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 Started per Catalog. Indian-market vendors include ASSA ABLOY Vingcard, dormakaba Saflok, Salto KS, Onity, ZKTeco — story body should be vendor-agnostic via the @hmp/hardware SmartLockAdapter interface (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 /grc generates 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) writes Reservation.status='CHECKED_IN' on the frontdesk local read-model, (b) mirrors to reservation-service canonical via ReservationServiceClient, (c) publishes RESERVATION_CHECKED_IN Kafka 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" (polls GET /api/reservations/:id up to 30 s) — PASS for the Indian guest path; S3 equivalent for international path also PASS once intlReservation is 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_IN event 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 ReservationServiceClient call 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 endpoint POST /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-service consumes RESERVATION_CHECKED_IN and dispatches via the templating engine (@hmp/template-engine block-tree → MJML → HTML; channel renderers for SMS/WhatsApp/push/in-app). Default templates: 5-channel __fallback strings in @hmp/notification-contracts v1 events (per shared-packages CLAUDE.md). Tenant per-property overrides via the visual editor (Workstream A). Consent matrix: enforces GuestCommunicationPreference{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.email or Guest.phone non-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 the communication.dispatched event 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 make Guest.marketingConsent a DERIVED column from the per-channel × per-category matrix. [SME-PROPOSED] new story US-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​

#ScenarioExpected behaviourCoverage todayOwning story / drift
F1Invalid Aadhaar format (5-digit 12345)400 with AADHAAR_INVALID_FORMATflow-6 S2 — FAIL by design (only 4xx because of pre-existing CHECKED_IN state, not because validator fires)D-005A-003 [P0 launch-blocker]
F2International guest, no passport supplied (idDocumentType=PASSPORT, empty formC.passportNo key)400 with PASSPORT_REQUIRED / FORMC_PASSPORTNO_REQUIREDflow-6 S4 — FAIL by design (FINDING-F6-04: class-validator absent-key bug)D-005A-003 follow-up [P0]
F3HK status not flipped to DIRTY / OCCUPIED_NEEDS_SERVICE after check-inRoom.housekeepingStatus transitions in HK-service replica AND surfaces on GET /housekeeping/rooms/statusflow-6 S5 — soft-warn todayD-005A-004 [P0 launch-blocker]
F4Key card encoder offline at check-inCheck-in proceeds (non-blocking per P1-A5); dashboard surfaces "encoded on retry — please re-encode" warning; staff can POST /keycards/reencode laterkeycard.service.spec.ts retry-with-backoff specs — PASSUS-005-013
F5Target room not VACANT_CLEAN (OCCUPIED / DIRTY / OUT_OF_ORDER)400 with ROOM_NOT_AVAILABLE and reason codecheckin.service.spec.ts :: "occupied/dirty room rejection" — PASSUS-005-001
F6Double check-in (reservation already CHECKED_IN)409 with RESERVATION_ALREADY_CHECKED_INImplicit in S2 — PASS (409 returned today)US-005-001
F7Walk-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-inscheckin.service.spec.ts walk-in path — PASS; saga recovery via POST /walk-in/:id/recover (P1-A2, Gotcha 16)US-005-010
F8Reservation in PENDING (not CONFIRMED) state400 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 — PASSUS-005-001 + D-005A-007 [P2] for UC gap
F9Arrival 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 feecheckin.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
F10Express check-in for foreign guest (FRRO gap)400 with EXPRESS_CHECKIN_INDIAN_NATIONALS_ONLY; staff path required to enforce FRRO Form C + visa expiryNot implemented today (gap)US-005-012 [SME-PROPOSED] AC
F11Aadhaar UID stored in full (no masking) on guest profileDPDPA Act 2023 §8(4) violation — full-decrypt access without AuditLog row writtenVerify in Task 6 — id-scanner.service.ts Aadhaar QR XML parser pathEP-004 D-006 [P0 launch-blocker] + US-005-015 [SME-PROPOSED] verification
F12Buyer GSTIN supplied with state-prefix mismatching property's state400 with BUYER_GSTIN_STATE_MISMATCH (if D-005A-009 [P0] implemented) — drives CGST/SGST vs IGST split at folio settlementNot implemented today (gap)D-005A-005 [P1] + D-005A-009 [P0]
F13C-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 — PASSUS-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 CheckInDto with buyerGstin + 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/current returns 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-documents S3 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.

IDSourceSeverityDescriptionSurfaced by step
D-005A-003EP-005AP0 (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-004EP-005AP0 (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-006EP-005AP1FINDING-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-005EP-005AP1Buyer 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-007EP-005AP2D1 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-016EP-005AP1Registration 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-001EP-005AP1Catalog 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-002EP-005AP1Catalog 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-006EP-004P0 (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-007EP-004P0 (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-011EP-004P1No 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-003EP-006P0 (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-04EP-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-GUARDSBF-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-CBF-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-CASCADEBF-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-DISPATCHBF-003 (new story)P1Catalog 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-TEMPLATEBF-003 (new story)P2Catalog 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-REFRESHBF-003 (new story)P2Catalog 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.yamlBF-003 (UC proposal)P2UC 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.yamlBF-003 (UC proposal)P2UC 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​

StepSpansWhy cross-EPIC
1 → 2EP-005AArrivals board entry; D1 reconciler is the cross-service-with-no-UC seam
3 → 4EP-005A ↔ EP-004Identity 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.
5EP-005A ↔ EP-004C-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).
6EP-005A ↔ EP-005B (folios, out of scope)Folio bootstrap inside the check-in transaction. Folio settlement at check-out is BF-005.
7 → 8EP-005A ↔ EP-002 ↔ EP-006Room 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 → 10EP-005A + @hmp/hardwareHardware integration is non-blocking; check-in proceeds even when encoder is offline.
11EP-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.
12EP-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.