BF-002 — Guest Booking (Search → Confirmation)
Business flow (cross-EPIC user journey). Reconciled 2026-05-25 as the sixth flow in the RTM Reconciliation programme (Wave 2 of the 12-flow scale-out). Spans EP-011 Booking Engine, EP-003 Reservation Management, EP-015 Integrations & Third-Party Connectors (Razorpay), EP-004 Guest Management (guest auto-create + DPDPA consent), and EP-012E Notifications (confirmation email + WhatsApp / SMS link).
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 gaps the RTM reconciliation programme is designed to surface.Property #1 launch context: BF-002 is rank-6 in the playbook's recommended sequence (BF-003 / BF-005 / BF-004 / BF-006 are higher priority because they own check-in / check-out / POS / night audit which are pure launch-blockers). BF-002 is revenue-critical post-launch — every direct booking that bypasses the OTAs avoids 12-18 % commission. As of 2026-05-16 demo readiness,
flow-3-booking-engine.spec.tswas 4/7 PASS with the documented failures concentrated in S2 (cross-service guest create FK race + Prisma client stale schema) and S3/S4 (payment-service flag flip not promoted).
Personas
| Persona | Role | Source |
|---|---|---|
| Guest (anonymous) | Prospective traveller searching for a room | US-011-001 / US-011-003 |
| Returning guest | Has booked at the same property or chain before — pre-fill on email | US-011-003 (lookup-by-email) |
| Front-desk agent | Receives the inbound reservation in the dashboard for room assignment | US-003-008 |
| Revenue manager | Manages rate plan setup, promo codes, and the "Best Value" flagging | US-002-005 / US-011-008 |
| Razorpay platform | External payment processor; webhook-driven settlement | EP-015 US-015-001 |
Entry points
| Entry | URL | Backed by |
|---|---|---|
| Standalone booking engine | https://<book-host> resolved via runtime host-mapping (e.g. book.thegrandpalace.com → propertyId) | hmp-booking-engine/src/hooks/useHostBootstrap.ts → GET /booking-engine-config/resolve-host |
| Widget embed | Hotel website embeds <script src=".../embed/widget.js"> and calls HMPBooking.init({ apiKey, propertyId }) | hmp-booking-engine/src/widget-entry.tsx |
| Iframe embed | <iframe src="https://book.<host>/?propertyId=…"> | hmp-booking-engine/src/App.tsx (mode dispatcher) |
| Chain hub multi-property cart | book.<chain>/chain BookingTrip multi-leg flow | hmp-booking-engine/src/pages/chain/ + bookingTripsApi |
| OTA channel (out of scope for BF-002 — covered in BF-007) | OTA → ChannelBookingReceivedConsumer | hmp-channel-service |
The 11-step journey
Step 1: Guest lands on booking engine (host resolution)
- Story: US-011-006 "Connect to Live Backend API" (post-2026-05-18 reframed: "Runtime Host Resolution + Live Backend Integration" per EP-011 D-007).
- Code:
hmp-booking-engine/src/hooks/useHostBootstrap.tscallsGET /booking-engine-config/resolve-host?host=<window.location.hostname>→hmp-property-service/src/host-mappings/host-mappings.controller.ts(@Public). Returns{ propertyId, brandId, tenantId, theme, publishableApiKey, mode }.hmp-booking-engine/src/App.tsx::AppWithThemedispatches one of 4 modes: PROPERTY / BRAND / TENANT / CHAIN_HUB. The returnedpublishableApiKeyis stored inhmp-booking-engine/src/lib/api/client.ts::setApiKey()for all subsequent calls. - Test:
hmp-booking-engine/src/App.PropertyBootstrap.test.tsx+hooks/useHostBootstrap.test.ts— PASS.hmp-property-service/src/host-mappings/host-mappings.controller.spec.ts+host-mappings.service.spec.ts— PASS. E2E:hmp-booking-engine/e2e/host-bootstrap.spec.ts. - Pre-conditions: DNS A-ALIAS for
<book-host>resolves to the per-env ALB (e.g.*.book.test.keynova.aiper Plan-1 wildcard cert).host_mappingsrow exists in property-service Postgres mappinghost → (tenantId, propertyId, brandId?). - Post-conditions: SPA bootstraps with
propertyId, theme tokens applied (1 of 9 KeyNova presets perbooking-engine-themeskill), per-property publishable API key cached for the session. - Notes: A5 (2026-05-22) made the API key per-(tenant,property) returned at runtime — pre-A5 the SPA shipped a static
hmp-default-public-booking-engine-keywhich is now DEV-ONLY. Widget mode bypasses host resolution entirely (HMPBooking.init({ apiKey })); cross-mode parity is untested per EP-011 US-011-013 SME note.
Step 2: Guest searches availability (dates + occupancy)
- Story: US-011-001 (Room Search with Availability Calendar) + US-003-006 (Real-Time Availability Engine — server side).
- Code:
hmp-booking-engine/src/components/SearchForm.tsxcollects (checkIn, checkOut, adults, children, rooms, promoCode?).hmp-booking-engine/src/hooks/useAvailability.ts(TanStack Query, 2-minstaleTime) POSTs to/api/booking-engine/search/availability(or GETs/booking-engine/availabilityfor the legacy path; both honour@SetMetadata(REQUIRE_API_KEY, true)per W0.2.2 2026-05-06). Gateway routes viahmp-api-gateway/src/config/services.config.ts:1257(/booking-engineprefix) →hmp-reservation-service/src/booking-engine/booking-engine.controller.ts:70-91→booking-engine.service.ts::checkAvailability()→searchAvailability(). Reservation-service consults the local property/room-type read model and callsavailability.service.ts::checkAvailability()which deriveseffectiveCapacity = totalRooms + bufferRooms(P0-2 overbook buffer) minus existingRoomReservationrows and active inventory holds. - Test:
hmp-reservation-service/src/availability/availability.service.spec.ts— PASS (incl. overbooking buffer).hmp-booking-engine/src/components/__tests__/SearchForm.test.tsx:: 6 specs — PASS. Flow:flow-3-booking-engine.spec.ts:: "S1 — POST /api/booking-engine/search/availability returns room types with availability" — PASS (per 2026-05-16 13-flow-demo-readiness snapshot, 4/7 passing subset). E2E:hmp-booking-engine/e2e/booking-flow.spec.ts. - Pre-conditions: Step 1 completed;
x-api-keyheader present (the publishable key from Step 1); date range valid (not in past, ≤ 30 nights per backend search-time guard). - Post-conditions: Response shape
{ roomTypes: [{ id, name, photos, amenities, ratePlans: [{ id, code, totalAmount, perNightRate, cancellationPolicy }], availableCount }] }. React-Query caches it for 2 min keyed by(propertyId, checkIn, checkOut, occupancy). - Notes: The 30-night max-stay rule and the 2-min
staleTimetogether create the D-002 from EP-011 race: two browsers on a peak night (Diwali / NYE) both seeavailableCount=1and both proceed to checkout. TheBookingTripmulti-leg cart addedexpiresAt = now + 10min2026-05-19 for chain hub trips, but single-property search has no equivalent hold. SME-PROPOSED: surface "Reserved for 10 minutes" badge +POST /availability/holdon Select. Calendar grid disable for sold-out nights uses the same response.
Step 3: Guest views room + rate options (BAR, promos, multi-currency display)
- Story: US-011-002 (Rate Plan Display & Comparison) + US-011-010 (Urgency Indicators & Social Proof — "Only 2 rooms left", "5 people viewing this room").
- Code:
hmp-booking-engine/src/components/booking/RoomList.tsx+RoomCard.tsx(line 113 —availableCount <= 3triggers urgency badge) +RatePlanSelector.tsx. Rate plan fetch viaGET /booking-engine/rate-plans/:propertyId(hmp-reservation-service/src/booking-engine/booking-engine.controller.ts:103) which aggregates fromhmp-property-service/src/booking-engine-support/booking-engine-support.controller.ts(internal service-account JWT). Social proof:GET /booking-engine/social-proof?propertyId=…(booking-engine.controller.ts:285-291) returns{ recentBookingsLast24h, currentOccupancyPct }. - Test:
hmp-booking-engine/src/components/__tests__/RoomCard.test.tsx:: 22 specs (info, gallery, amenities, urgency badge, rate plan selection) — PASS.PriceBreakdown.test.tsx:: 37 specs (BookingSummary, PriceDisplay, currency, GST, discounts) — PASS.hmp-reservation-service/src/booking-engine/booking-engine.service.spec.ts::getRatePlans+getSocialProof— PASS. - Pre-conditions: Step 2 returned at least 1 available room type.
- Post-conditions: User scrolls a list of
RoomCards, each showing room photos + amenities + 1-N rate plan options with per-plan price + cancellation policy summary. Best Value badge currently computed client-side (no server-sideisRecommendedflag — EP-011 D-003 SME-PROPOSED). - Notes: GST display is the most expensive bug in the funnel:
BookingSummary.tsxhard-codessubtotal * 0.12(flat 12 % GST) but the actual Indian slab is{ ≤₹1,000: 0%, ₹1,001-7,500: 12%, ₹7,501+: 18% }per the property'sTaxConfiguration(BF-001 Step 10, D-005). Razorpay charges what the page shows, so a ₹8,000+/night room is silently under-billed by 6 percentage points. EP-011 D-003 is P0. Multi-currency / forex display (USD/EUR for foreign guests) isNot Started(EP-011 US-011-015 /[SME-PROPOSED]US-011-NEW-FOREX). The 14-language UI translation IS shipped (EP-011 US-011-009 Verified-Built — all 14 locale files including SE Asia th/vi/id/ms registered insrc/lib/i18n.ts).
Step 4: Guest selects room + rate plan (+ optional promo code)
- Story: US-011-001 continued (selection state) + US-011-008 (Promo Code Validation — Server-Side).
- Code: Selection writes to
hmp-booking-engine/src/stores/bookingStore.ts(Zustand). Promo-code path:hmp-booking-engine/src/components/booking/PromoCodeInput.tsx→hooks/usePromoCode.ts→POST /booking-engine/promo-codes/validate(booking-engine.controller.ts:163-168, public + API-key) →booking-engine.service.ts:2033::validatePromoCode()→hmp-reservation-service/src/promo-codes/promo-code.service.ts. Returns{ valid, discountType, discountAmount, restrictions, ratePlanIds[] }. - Test:
hmp-reservation-service/src/promo-codes/promo-code.service.spec.ts— PASS.hmp-reservation-service/src/booking-engine/booking-engine.service.spec.ts:1283+::validatePromoCodespecs — PASS. E2E:hmp-booking-engine/e2e/booking-full-flow.spec.ts(promo entry within flow). - Pre-conditions: Step 3 completed; user clicks "Select" on a (roomType, ratePlan) pair; if promo entered, server returns
valid=true. - Post-conditions: Booking store carries
{ propertyId, roomTypeId, ratePlanId, checkIn, checkOut, adults, children, promoCode?, discountAmount? }. UI advances to GuestForm step. - Notes: Promo discount can trigger a GST slab re-evaluation (₹8,000 → 10 % off → ₹7,200 crosses 18 %→12 % boundary) — today the discount is applied AFTER GST is computed at the wrong rate, double-bug (EP-011 D-010 P1). No abandoned-cart recovery if the guest bounces here (EP-011 D-011 SME-PROPOSED
US-011-NEW-ABANDONED-CART). No re-validation of the promo at booking-create time — a code valid at apply-time but exhausted by booking-time silently overcharges (EP-011 US-011-008 SME note).
Step 5: Guest enters details (name, email, phone, ID, special requests)
- Story: US-011-003 (Guest Details Form with Validation) + US-004-001 (Guest Profile CRUD — server-side auto-create).
- Code:
hmp-booking-engine/src/components/GuestForm.tsxcollects{ firstName, lastName, email, phone, idType?, idNumber?, gstin?, specialRequests? }. Returning-guest pre-fill: on email blur, callshmp-reservation-service/src/booking-engine/booking-engine.service.ts::lookupGuestByEmail()which proxies tohmp-guest-serviceGuestServiceClient.findByEmail(propertyId, email)(5-min cache). DTO validation:hmp-reservation-service/src/booking-engine/dto/booking-engine.dto.ts::CreateBookingDto. Aadhaar masking for display inGuestForm.tsx; Verhoeff validator lives inhmp-guest-service/src/guests/guests.service.tsfor the persist side. - Test:
hmp-booking-engine/src/components/__tests__/GuestForm.test.tsx— PASS.hmp-guest-service/src/guests/guests.service.spec.ts:: Aadhaar Verhoeff + PAN regex + sensitive-data masking — PASS. - Pre-conditions: Step 4 completed.
- Post-conditions: Booking store now carries the guest payload; form valid.
- Notes: EP-011 D-004 (P1) — Aadhaar masking is enforced on display but no automated assertion that the API submission drops the full 12-digit value. DPDPA art. 8 + Aadhaar Act §29(2) require only the last 4 digits be persisted unless purpose-bound consent is captured; today there is no
consentGivenForFullAadhaarcheckbox + noidHash(SHA-256 dedup) submission contract. Phone auto-prepends+91— a US guest cannot enter+1-555-...without switching browser locale. GSTIN regex (per BF-001 Step 10) is enforced client-side only — the booking-engine should reuse the property-service / guest-service validator, not roll its own. For under-18 guests, the EP-004dpdpa-children-consent.flow.spec.tsrequires guardian fields (guardianName,guardianRelation,guardianPhone,guardianConsentAt) — not enforced by the booking-engine GuestForm today (consider as[SME-PROPOSED]).
Step 6: Guest sees price breakdown (room + tax + fees)
- Story: US-011-002 (price display continued) +
[SME-PROPOSED]US-011-NEW-INTERSTATE-GST"Inter-state vs intra-state GST split (CGST+SGST vs IGST)". - Code:
hmp-booking-engine/src/components/booking/BookingSummary.tsx+PriceDisplay.tsxrenderΣ perNightRate × nights + taxes - discount. Backend pricing preview:hmp-reservation-service/src/reservations/reservations.service.ts::preview()(called viaPOST /booking-engine/preview-pricing) which invokeshmp-reservation-service/src/promotions/promotion-service.client.ts::applyPromotions()(AP-T11-014) +GstService.getRoomGstRate()(slab logic — gotcha 12). Tax-inclusive vs tax-exclusive flag honoured via AP-T03-004 (isTaxInclusive) for OTA path; direct booking is always tax-exclusive today. - Test:
hmp-booking-engine/src/components/__tests__/PriceBreakdown.test.tsx:: 37 specs (currency, GST, discounts — but NO per-slab GST coverage, EP-011 D-003) — PASS.hmp-reservation-service/src/reservations/reservations.service.spec.ts:: AP-T03-004 OTA tax-inclusive specs — PASS. - Pre-conditions: Steps 4 + 5 completed.
- Post-conditions: Guest sees
{ subtotal, discount, taxes, total }matching what Razorpay will charge. - Notes: 3 unsurfaced gaps the SME must adjudicate:
- GST slab (EP-011 D-003 P0): hard-coded 12 % vs per-slab
{0%, 12%, 18%}. Drives the wrong total → wrong Razorpay charge. - CGST/SGST vs IGST split: today only a single
gstAmountis shown. For inter-state guests (guest billing address state ≠ property GSTIN state), the e-invoice + folio MUST split as IGST 18% (not CGST 9% + SGST 9%) — this is determined at booking time and locks the invoice format.[SME-PROPOSED]add AC under EP-011 US-011-005 (confirmation/voucher already has this SME-PROPOSED — see Step 10). - Resort fees / property fee: Indian-hospitality reality has
Property.dailyResortFeeandProperty.cleaningFeeas per-night additions in some chains. Today the booking-engine does not surface these as line items.
- GST slab (EP-011 D-003 P0): hard-coded 12 % vs per-slab
Step 7: Guest initiates payment (Razorpay hosted / payment link / UPI QR)
- Story: US-011-004 (Razorpay Payment Integration) + US-015-001 (Razorpay Payment Gateway Integration).
- Code:
hmp-booking-engine/src/components/booking/PaymentMethods.tsxlets guest pick UPI / card / netbanking / wallet.hmp-booking-engine/src/components/PaymentForm.tsxorchestrates create-reservation → initiate-payment. Reservation create (idempotent):POST /booking-engine/reservations(booking-engine.controller.ts— API-key gated@Public) →booking-engine.service.ts::createBooking()(callsGuestServiceClient.findOrCreateGuestByEmailthenReservationsService.create()withsource='BOOKING_ENGINE'+ idempotency-key fromIdempotency-Keyheader per AP-T03-001). Payment initiate (proxy path, flag-gated):POST /booking-engine/payments/initiate-v2(booking-engine.controller.ts:225-237) →booking-engine.service.ts:1784::initiatePaymentV2()resolves(tenantId, propertyId, totalAmount)server-side from the reservation row, forwards toPaymentServiceClient.initiatePayment(service-account JWT) →hmp-payment-service/src/payments/payment.controller.ts::initiate()→payment-orchestrator.service.ts→RazorpayGatewayAdapter→ Razorpay Orders API. Returns{ paymentOrder: { id, gatewayOrderId, amount, currency }, razorpayKeyId? }. The browser then opens Razorpay Checkout viahmp-booking-engine/src/hooks/useRazorpayCheckout.ts+lib/razorpay.ts. - Test:
hmp-payment-service/src/adapters/razorpay/razorpay-gateway.adapter.spec.ts— PASS.hmp-reservation-service/src/booking-engine/booking-engine.service.spec.ts— service-level — PASS. Flow:flow-3-booking-engine.spec.ts:: "S3 — POST /api/booking-engine/payments/initiate returns orderId via proxy" — PARTIAL (passes 200 + orderId shape; S2 reservation-create fallback to JWT path triggers a console.warn).payment-links.flow.spec.ts+upi-qr.flow.spec.ts— TBD.hmp-booking-enginehas NO PaymentForm / PaymentMethods / useRazorpayCheckout unit specs (EP-011 D-006 P1). - Pre-conditions: Steps 5 + 6 completed; reservation created with
status=PENDING; merchant-account configured on the property (Razorpay test/live keys perMerchantAccountrow);Tenant.isDemoTenanthonoured (demo-tenant guard rejectsrzp_live_*perhmp-payment-service/src/demo-tenant/demo-tenant.service.ts—DEMO_TENANT_REQUIRES_SANDBOX_KEY). - Post-conditions: Razorpay
Orderexists (gatewayOrderId returned). LocalPaymentrow in payment-service Postgres withstatus='INITIATED'. The booking-engine SPA holds(razorpayOrderId, razorpayKeyId)and opens the hosted Checkout modal. - Notes: EP-011 D-005 (P0) —
VITE_FEATURE_PAYMENT_SERVICE_ENABLEDdefaults OFF in production per platform CLAUDE.md. The flag-OFF legacy path goes through/booking-engine/payments/initiate(NOT-v2) which trusts client-suppliedamount— tamper-risk. The flag-ON proxy path re-derivesamountfromreservation.totalAmountserver-side — secure. Until the flag flip ships, the secure code path is unreachable in prod. AP-T12-004PaymentReconcilecron sweeps stuckINITIATEDpayments every 5 min to prevent ghost orders.
Step 8: Razorpay redirect + webhook (verify + capture)
- Story: US-011-004 continued (verify) + US-015-001 continued (webhook) + US-003-015 (Payment Initiation & Verification — Razorpay).
- Code: Two parallel paths converge:
- Redirect (synchronous) — Razorpay redirects browser to the SPA's success URL with
(razorpay_payment_id, razorpay_order_id, razorpay_signature). SPA posts these toPOST /booking-engine/payments/verify→booking-engine.service.ts:2008::verifyPayment()(W1.A.2 fail-closed contract 2026-05-06 — missing/invalid signature returns{success:false}with NO state change). Payment-service path:PaymentServiceClient.verifyRedirect()→payment.controller.tsPOST /payments/:id/verify-razorpay-redirect→razorpay-signature.tsHMAC-SHA256 check. - Webhook (asynchronous, authoritative) — Razorpay POSTs
payment.captured(orpayment.failed) toPOST /api/webhooks/razorpay-gateway/:merchantAccountId(hmp-payment-service/src/webhooks/webhook.controller.ts—@Public, raw body viabody-parser.rawin main.ts).webhook-dispatcher.service.ts::verifyAndParseWebhook()checks HMAC + dedups. On capture,payment-event-publisher.service.tsemitspayment.capturedvia the outbox →hmp-frontdesk-servicePaymentCapturedConsumer(20/20 specs) writes a folioPAYMENTtransaction with dedup keyfolio:{folioId}:payment-captured:{paymentId}and flipsReservation.paymentStatus='PAID'+Reservation.status='CONFIRMED'. Reconcile cron:webhook-reconcile.service.tsrequeues stuckRECEIVEDevents > 5 min old.
- Redirect (synchronous) — Razorpay redirects browser to the SPA's success URL with
- Test:
hmp-payment-service/src/webhooks/*.spec.ts— PASS.hmp-payment-service/src/adapters/razorpay/razorpay-signature.spec.ts— PASS.hmp-frontdesk-service/src/folios/__tests__/payment-captured.consumer.spec.ts:: 20/20 specs — PASS. Flow:flow-3-booking-engine.spec.ts:: "S4 — POST /api/booking-engine/payments/verify enforces HMAC signature (W1.A.2 fail-closed contract)" — PASS (asserts bogus signature →{success:false}+ reservation status unchanged).payments.flow.spec.ts:: "Razorpay end-to-end" — TBD. - Pre-conditions: Step 7 completed; Razorpay processed the payment (test mode in dev/qa, live in prod).
- Post-conditions:
Payment.status='CAPTURED'.Reservation.status='CONFIRMED'.Folio.balanceDuereduced byPayment.amount(Indian INR-in-paise round-trip via@hmp/payments::toPaise/fromPaise). Outbox eventRESERVATION_CONFIRMEDqueued. - Notes: The redirect path is for UX only — the webhook is the source of truth. A guest who closes the browser before redirect still gets a confirmed reservation when the webhook arrives. The 2026-05-19 Folio Payment Reconciliation 7-phase plan added the
payment.captured→ folio dedup loop. Refund symmetry is NOT yet wired (EP-011 D-009 P0):cancelBooking()returns refund amount but does NOT call Razorpay Refunds API.PaymentRefundedConsumeris[SME-PROPOSED]. Razorpay paise round-trip (gotcha 8): thetoPaise/fromPaisehelper exists in@hmp/paymentsbut no spec asserts round-trip equivalence on edge values (₹0.50, ₹1,00,000) — EP-003 US-003-015 SME-PROPOSED.
Step 9: Reservation confirmed (status=CONFIRMED, outbox publish)
- Story: US-003-001 (Create Individual Reservation — covers the confirmation transition) + US-003-017 (Reservation Modification History — first row written on create).
- Code:
hmp-reservation-service/src/reservations/reservations.service.ts::create()already ran during Step 7 in a transaction that also writes aReservationHistoryrow and emitsreservation.createdvia Kafka (Redis Streams in dev). When thepayment.capturedwebhook lands (Step 8),PaymentCapturedConsumerpatchesReservation.paymentStatus='PAID'and (if the rate plan requires payment-on-arrival vs payment-now) flipsstatus='CONFIRMED'. The transactional outbox writes theRESERVATION_CONFIRMEDevent in the same DB transaction as the status flip; a background dispatcher publishes to Kafka with at-least-once delivery. Confirmation number generated bygenerateConfirmationNumber(prefix)— prefix defaults toRES(NOTProperty.code— EP-003 D-011 SME-PROPOSED). - Test:
hmp-reservation-service/src/reservations/reservations.service.spec.ts:: happy-path + FU-55 analytics emission + AP-T03-004 OTA tax-inclusive + tenant-ownership-error mapping — PASS.reservations.sprint2.spec.ts:: confirmation number format + 13 BookingSource enum coverage + sourceReference + Kafka event payload assertions — PASS. Flow:flow-3-booking-engine.spec.ts:: "S6 — GET /api/reservations/:id has guest, source, totalAmount, balanceDue, rate-plan, and dates" assertsstatus === 'CONFIRMED'after S4 — PARTIAL (depends on S2 succeeding via primary or fallback path). - Pre-conditions: Step 8 webhook arrived OR the rate plan is "pay-at-property" (status flips to
CONFIRMEDimmediately on create, no payment required). - Post-conditions:
Reservation.status='CONFIRMED',confirmationNumberpopulated,source='BOOKING_ENGINE',ReservationHistory[0]row written,Foliorow exists in reservation-service read model AND in frontdesk-service write model. Outbox publishesRESERVATION_CONFIRMEDevent. - Notes: EP-011 US-011-006 / D-007 flagged a known cross-service bug: in some compose environments
GuestServiceClient.findOrCreateGuestByEmailreturns a guest ID that doesn't yet exist in reservation-service's localguestsread-model →reservations_guestId_fkeyviolation. Theflow-3-booking-engine.spec.tsS2 fallback path documents this as a "Phase 2 deferred" cross-service migration item. Until fixed, S2 falls back to the JWT-authenticated/api/reservationspath withsource='BOOKING_ENGINE'to keep S3-S6 testable.
Step 10: Confirmation email + WhatsApp / SMS dispatched
- Story: US-011-005 (Booking Confirmation Page & Email) + US-003-016 (Payment Link Generation & Delivery — for pay-at-property bookings).
- Code:
hmp-reservation-service/src/booking-engine/booking-engine.controller.ts:296-305POST /booking-engine/notifications/booking-confirmation→booking-engine.service.ts::sendBookingConfirmation()publishes a Kafka event consumed byhmp-notification-servicewhich renders viahmp-shared-packages/packages/template-engine(MJML block-tree → HTML for email;@hmp/notification-contractsper-channel__fallbackstrings for SMS / WhatsApp / push / in-app) and dispatches via AWS SES (per 2026-05-21 migration; was SendGrid).hmp-booking-engine/src/components/booking/ConfirmationPage.tsx+BookingConfirmation.tsxrender the in-browser confirmation while the email is in flight. PDF voucher generator lives inhmp-frontdesk-serviceinvoice-pdf module (not enumerated yet for booking-engine path — see Notes). - Test:
hmp-reservation-service/src/booking-engine/booking-engine.service.spec.ts::sendBookingConfirmationKafka publish test — PASS. Flow:flow-3-booking-engine.spec.ts:: confirmation step is asserted transitively via S5/S6 dashboard read-back. E2E:hmp-booking-engine/e2e/booking-full-flow.spec.tsverifies confirmation page renders post-payment. - Pre-conditions: Step 9 completed (
RESERVATION_CONFIRMEDevent published). - Post-conditions: Email arrives at guest's inbox (subject "Booking confirmed at
", body with confirmation number + IST check-in/check-out + room type + total + CGST/SGST/IGST split + cancellation summary + property contact). For WhatsApp-opted-in guests, a transactional WhatsApp message via pre-approved Meta template. For SMS, a DLT-registered transactional template (TRAI DND compliance — DND doesn't block transactional, but the template must be registered). - Notes: 3 SME-PROPOSED gaps:
Reservation.guestLanguagenot persisted (EP-011 D-012 P1): the post-checkout chain (check-in reminder T-24h, post-stay survey) regresses to property'sdefaultLocalebecause the booking-time locale isn't carried forward. SE Asia (th/vi) guests get English follow-ups.- PDF voucher Thai/Vietnamese font embedding (EP-011 D-012 P1):
hmp-shared-packages/packages/common/src/printing/ESC/POS receipt path doesn't embed Noto Sans Thai / Vietnamese fonts — Thai guests see broken glyphs in any printed voucher. - CGST/SGST vs IGST split in the email (EP-011 US-011-005 SME-PROPOSED): the confirmation email must compute the split based on
(propertyGstinState, guestBillingAddressState). Today the template-engine receives a singlegstAmount— no split logic. Same root cause as Step 6.
Step 11: Reservation visible in dashboard (front-desk read)
- Story: US-003-003 (Reservation Search & Filters) + US-003-009 (Arrivals / Departures / In-House Boards) + US-003-008 (Room Assignment, for the agent's next action).
- Code:
hmp-web-dashboard/src/app/(dashboard)/reservations/page.tsxcallsGET /api/reservations?propertyId=…&fromDate=…&toDate=…→hmp-reservation-service/src/reservations/reservations.controller.ts::search()(POST/reservations/searchwithSearchReservationsDto; also GET list with query). Filters: status[], source[], guestName (AP-T04-003 local Guest read-model OR, case-insensitive), confirmationNumber, dateRange. Arrivals/Departures boards:GET /properties/:propertyId/arrivals?date=(duplicated in both reservation-service AND frontdesk-service — EP-003 D-009 P1). Front-desk room assignment:POST /reservations/assign-roomwritesRoomReservation.roomIdand emitsroom.assigned+reservation.modifiedwithchangeType='ROOM_MOVE'(AP-T05-007). - Test:
hmp-reservation-service/src/reservations/reservations.service.spec.ts:: search filter combinations + arrivals/departures/in-house list shape — PASS.search-q-param.spec.ts— PASS. Flow:flow-3-booking-engine.spec.ts:: "S5 — GET /api/reservations?arrival_date lists new reservation with correct name, room type, dates" — PASS (withpollUntilto tolerate the same stale-read-model race called out in BF-001 D-001). E2E:hmp-web-dashboard/e2e/tests/reservations-list.spec.ts,tests/15-crud-reservations.spec.ts. - Pre-conditions: Step 9 completed; front-desk agent logged in (
FRONT_DESK/MANAGER/ADMINrole) and has access to the property. - Post-conditions: Reservation row appears in dashboard's Reservations list AND in the Arrivals board for the check-in date. Agent can: assign room (US-003-008), modify (US-003-004), send payment link for remaining balance (US-003-016), or cancel (US-003-005). Confirmation number
<prefix><YYYYMMDD><SEQ>searchable in < 200 ms p95 (claimed SLA; no load test asserts it — EP-003 D-003). - Notes: EP-003 D-009 (P1) —
/arrivalsexists in BOTH reservation-service AND frontdesk-service and is NOT guaranteed to return the same row set. The dashboard reads from reservation-service; the front-desk check-in flow reads from frontdesk-service. A row visible on the dashboard arrivals board may be invisible to the check-in cascade (or vice versa). Departures-with-outstanding-balance must read live folio (NOT the synced read model) to avoid stale-balance flagging. The stale-read-model race surfaces inflow-3.spec.tsS5 via thepollUntilworkaround.
Failure modes worth testing
| # | Scenario | Expected behaviour | Coverage today | Owning story |
|---|---|---|---|---|
| F1 | Payment failed (insufficient funds / card declined) | Razorpay payment.failed webhook; reservation stays PENDING; SPA shows error; guest can retry | hmp-payment-service/src/adapters/razorpay/*.spec.ts covers signature + error mapping. End-to-end retry NOT covered by flow-3.spec.ts. | US-011-004 / US-015-001 |
| F2 | Razorpay signature invalid (tampering attempt) | verifyPayment() returns {success:false}; reservation status UNCHANGED (W1.A.2 fail-closed contract); no folio mutation | flow-3-booking-engine.spec.ts :: S4 — PASS (security contract verified) | US-011-004 |
| F3 | Duplicate booking submission (browser double-click) | Idempotency-Key header per AP-T03-001 returns the same reservation on replay; no second Reservation row | idempotency-replay.flow.spec.ts — exercises POST /reservations replay | US-003-001 |
| F4 | Inventory race — two guests both see availableCount=1, both submit | First write wins; second gets 409 OVERBOOKED (effective capacity includes overbooking buffer) | availability.service.spec.ts overbook buffer + reservations.service.spec.ts OVERBOOKED errorCode path — PASS. NO concurrency stress test. EP-011 D-002 / EP-003 D-007 [SME-PROPOSED] add lock-or-quote inventory hold pattern | US-011-001 / US-003-006 / US-003-007 |
| F5 | Abandoned cart (guest reaches payment, closes browser) | Today: no recovery (Redis hold expires after 15 min if held; reservation stays PENDING). SME-PROPOSED: abandoned-cart email at +1h/+24h/+72h | NONE today — EP-011 D-011 P1 [SME-PROPOSED] US-011-NEW-ABANDONED-CART | US-011-008 / new |
| F6 | OTA double-booking (channel push + direct booking same inventory) | First write wins; channel-service emits channel.sync.failed event; revenue manager alerted | channel-booking-received.consumer.spec.ts covers the consumer side. Cross-channel race NOT covered. | EP-009 (channel) / US-003-006 |
| F7 | Promo code exhausted between apply-time and booking-time | Today: silently applies discount; user gets unauthorized discount. SME-PROPOSED: re-validate at createBooking() | NONE — EP-011 US-011-008 SME-PROPOSED | US-011-008 |
| F8 | Cross-tenant guest fetch (attacker passes another tenant's guest email to BE API) | 403 TENANT_MISMATCH from assertPropertyTenant() in receiver | guest-cross-tenant-denial.spec.ts — PASS (per EP-004 RTM; verify wired to default Jest pattern per EP-004 D-002) | US-004-001 / cross-cutting |
| F9 | Demo-tenant misconfigures Razorpay LIVE keys (e.g. tries rzp_live_* on demo tenant) | Payment-service rejects with DEMO_TENANT_REQUIRES_SANDBOX_KEY (demo-tenant guard, 3-tier lookup Redis → local → AuthServiceClient) | hmp-payment-service/src/demo-tenant/*.spec.ts — PASS | US-015-001 |
| F10 | Guest under 18 books without guardian consent | Today: booking-engine GuestForm doesn't enforce; backend accepts. SME-PROPOSED: front-end gate per EP-004 US-004-012 + DPDPA §9 | dpdpa-children-consent.flow.spec.ts covers backend; front-end gap | EP-004 / new |
Day-2 onboarding ("first booking" experience)
The hotel's first guest booking is a milestone the platform should celebrate AND instrument. Items the GM / revenue manager tackles in the first 30 days post first booking. Not in BF-002 scope but flagged so BF-003 (check-in) and follow-up flows know where to pick up:
- First confirmation email lands in spam? — SES sender domain
(
mail.<env>.keynova.aiper OP-1, 2026-05-21) DKIM + SPF + DMARC validation. If the property's domain is mapped via host-mapping (e.g.book.thegrandpalace.com), the From: address still usesmail.keynova.ai. SME-PROPOSED: per-property sender domain configuration with DKIM auto-issuance (future story). - First WhatsApp / SMS template approval — Meta WhatsApp Business API requires
pre-approved templates; TRAI DLT registration takes 3-7 days. Property must do
this before going live. Notification-service has
__fallbackstrings but delivery requires the approved template ID. - Razorpay merchant onboarding —
smoke-merchant-account.real.spec.tscovers UI; full saga inuc-tenant-payment-onboarding.yaml. Live keys require Razorpay KYC + bank verification (typically 3-5 business days). - First "Best Value" rate plan flagging — revenue manager wants to highlight
a specific plan; today client-side
isRecommendedonly. EP-011 US-011-002 SME-PROPOSED: tenant-managedisRecommendedboolean on RatePlan. - First promo code creation — typical "WELCOME10" 10% off for direct
bookings; revenue manager creates via
/api/promo-codes. No abandoned-cart recovery flow tied to promos today. - First refund — DPDPA art. 8(3) "reasonable period" interpretation = 7
business days. Today refunds are dashboard-manual via
/api/payments/refund(US-003-005 / EP-011 D-009 P0). - Cookie consent banner audit — DPDPA art. 7 requires per-category consent (Essential / Analytics / Marketing) — today's banner is "Accept all / Reject all" only (EP-011 US-011-014 SME-PROPOSED).
- Conversion funnel measurement — booking-engine funnel analytics is
Not Started(EP-011 US-011-015). Without funnel events, the GM has no data-driven view of "where guests drop off" — payment is the usual culprit (60-70 % of cart abandons per industry baseline). - Multi-currency display for foreign guests — INR-only today (EP-011
[SME-PROPOSED]US-011-NEW-FOREX). A US guest searching for a Goa resort sees ₹ and may bounce. - Loyalty enrolment at first booking — US-011-011 dashboard ships; the
"Points Redemption During Booking" flow that monetises loyalty is not
catalogued (EP-011 D-013 P1 SME-PROPOSED
US-011-017). - First channel-manager push — direct booking parity with OTAs requires rate parity rules + inventory sync. Covered in BF-007 OTA Channel Sync.
- Booking-engine widget embed on hotel marketing site — INTEGRATION.md +
test-embed.htmlfor testing; cross-mode parity untested (EP-011 US-011-013 Built-Untested).
Drift summary
The 12 BF-002-surfaced drifts that the SME pass must adjudicate. (P0 = blocks
Property #1 launch via revenue/compliance impact; P1 = ships next sprint;
P2 = backlog.) Drift IDs are namespaced BF-002-D-NNN to coexist with
per-EPIC EP-NNN D-NNN.
| ID | Type | Severity | Description | Owning EPIC drift | Surfaced by step |
|---|---|---|---|---|---|
| BF-002-D-001 | India-hospitality + tests-assert-behaviour-story-doesnt-describe | P0 | BookingSummary.tsx hard-codes flat 12 % GST instead of the per-tariff slab { ≤₹1,000: 0%, ₹1,001-7,500: 12%, ₹7,501+: 18% }. Razorpay charges what the page shows → rooms ≥ ₹7,501/night are silently under-billed by 6 ppts. | EP-011 D-003 | Steps 3, 6 |
| BF-002-D-002 | Catalog-honest + tampering-risk | P0 | VITE_FEATURE_PAYMENT_SERVICE_ENABLED defaults OFF in production; secure server-side amount revalidation in initiate-v2 proxy is unreachable in prod. Legacy /initiate path trusts client-supplied amount. | EP-011 D-005 | Step 7 |
| BF-002-D-003 | Story-vs-code + DPDPA + revenue-leak | P0 | cancelBooking() returns refund amount but does NOT call Razorpay Refunds API; refunds are dashboard-manual; DPDPA art. 8(3) 7-day reasonable-period SLA depends on staff diligence. PaymentRefundedConsumer SME-PROPOSED. | EP-011 D-009 | Step 8 (cancellation symmetry) |
| BF-002-D-004 | Story-vs-code + DPDPA + Aadhaar Act §29(2) | P1 | Aadhaar masking on display verified; no automated assertion that the API submission drops the full 12-digit value. consentGivenForFullAadhaar checkbox + idHash SHA-256 dedup contract SME-PROPOSED. | EP-011 D-004 | Step 5 |
| BF-002-D-005 | Test-vs-code + missing-coverage | P1 | PaymentForm.tsx, PaymentMethods.tsx, useRazorpayCheckout.ts have zero unit specs despite owning the highest-error-rate step in the funnel. | EP-011 D-006 | Step 7 |
| BF-002-D-006 | Story-incomplete + abandoned-cart-recovery | P1 | No abandoned-cart recovery flow: guest applies promo, bounces at payment → never re-engaged. Industry baseline 8-15 % lift on emailed abandons. New story US-011-NEW-ABANDONED-CART. | EP-011 D-011 | Step 4 / F5 |
| BF-002-D-007 | Story-incomplete + India-hospitality-critical + i18n | P1 | Reservation.guestLanguage not persisted; post-checkout chain (check-in reminder, post-stay survey) regresses to property's defaultLocale. SE Asia th/vi/id/ms guests get English follow-ups. | EP-011 D-012 | Step 10 |
| BF-002-D-008 | Cross-EPIC-handoff + missing-test | P1 | /arrivals endpoint exists in BOTH reservation-service AND frontdesk-service and is NOT guaranteed to return the same row set. Front-desk cascade may not see a row the dashboard shows. | EP-003 D-009 | Step 11 |
| BF-002-D-009 | Story-incomplete + India-hospitality-critical | P1 | Inter-state vs intra-state GST split (CGST+SGST vs IGST) determined by (property GSTIN state, guest billing address state) — required for valid e-invoice — NOT computed at booking time today. Single gstAmount sent to template engine. | none yet (cross-EPIC) | Steps 6, 10 |
| BF-002-D-010 | Catalog-gap + missing-story | P1 | "Points Redemption During Booking" (loyalty monetisation lever) documented in EPIC body §US-011-09 but has no catalog row. New catalog row US-011-017 SME-PROPOSED. | EP-011 D-013 | Step 7 (price recompute) |
| BF-002-D-011 | Cross-service-handoff + known-bug | P2 | flow-3.spec.ts S2 fallback documents a Phase-2 deferred cross-service bug: GuestServiceClient.findOrCreateGuestByEmail returns guest ID that doesn't yet exist in reservation-service's local read-model → reservations_guestId_fkey violation. Forces JWT-path fallback for test coverage. | EP-011 D-007 (catalog) | Step 9 |
| BF-002-D-012 | Story-example-misleading | P2 | Story body promises confirmation-number prefix is the property code (GPH20260415001); actual code defaults to RES (per safeguard config). Tenant configuring "GPH" sees all confirmations start with RES. | EP-003 D-011 | Step 9 |
Cross-EPIC narrative summary
| Step | Spans | Why cross-EPIC |
|---|---|---|
| 1 | EP-011 ↔ EP-002 (property) | Host resolution + per-property publishable API key live in property-service; SPA in booking-engine. Two services own one bootstrap. |
| 2 → 4 | EP-011 (BE) ↔ EP-003 (reservations) | Availability + rate plans + promo-code validation are API-key gated public endpoints on reservation-service under the /booking-engine/ prefix; the frontend is booking-engine. |
| 5 | EP-011 ↔ EP-004 (guest) | Guest auto-create / lookup-by-email straddles booking-engine UI, reservation-service proxy, guest-service write. Aadhaar masking + DPDPA consent are guest-service concerns surfaced in the booking flow. |
| 6 | EP-011 ↔ EP-002 (taxation) | GST slab + CGST/SGST/IGST split live in property-service TaxConfiguration but the math runs in booking-engine + reservation-service. Three-service tax surface is the largest BF-002 cross-EPIC seam. |
| 7 → 8 | EP-011 ↔ EP-015 (Razorpay) ↔ EP-003 (payment row) ↔ EP-005B (folio post via PaymentCapturedConsumer) | Payment is the four-service handoff: booking-engine SPA → reservation-service proxy → payment-service adapter → Razorpay → webhook → outbox → frontdesk consumer. The flag-OFF legacy path skips two of the four services and is the active P0 surface. |
| 9 → 10 | EP-003 ↔ EP-012E (notifications) | Reservation confirmation event → notification-service → SES email + WhatsApp + SMS. Template engine + per-event Zod schemas owned by EP-012E. |
| 11 | EP-003 (read) ↔ EP-005A (front-desk arrivals duplicate) | The arrivals/departures duplication seam (D-009 above) is THE classic "two services own one read" cross-EPIC drift. Front-desk agent's next move (room assignment, check-in) lives in EP-005A and is covered by BF-003. |
The "payment four-service handoff" (Step 7 → 8) and the "GST three-service
surface" (Step 6) are the two largest cross-EPIC seams BF-002 surfaces.
Combined with the catalog-gap on US-011-017 loyalty redemption (D-010),
the missing CGST/SGST/IGST split logic (D-009), and the flag-OFF payment
security regression (D-002), these are the primary BF-002 findings the SME
must act on.