Skip to main content

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.ts was 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​

PersonaRoleSource
Guest (anonymous)Prospective traveller searching for a roomUS-011-001 / US-011-003
Returning guestHas booked at the same property or chain before — pre-fill on emailUS-011-003 (lookup-by-email)
Front-desk agentReceives the inbound reservation in the dashboard for room assignmentUS-003-008
Revenue managerManages rate plan setup, promo codes, and the "Best Value" flaggingUS-002-005 / US-011-008
Razorpay platformExternal payment processor; webhook-driven settlementEP-015 US-015-001

Entry points​

EntryURLBacked by
Standalone booking enginehttps://<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 embedHotel 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 cartbook.<chain>/chain BookingTrip multi-leg flowhmp-booking-engine/src/pages/chain/ + bookingTripsApi
OTA channel (out of scope for BF-002 — covered in BF-007)OTA → ChannelBookingReceivedConsumerhmp-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.ts calls GET /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::AppWithTheme dispatches one of 4 modes: PROPERTY / BRAND / TENANT / CHAIN_HUB. The returned publishableApiKey is stored in hmp-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.ai per Plan-1 wildcard cert). host_mappings row exists in property-service Postgres mapping host → (tenantId, propertyId, brandId?).
  • Post-conditions: SPA bootstraps with propertyId, theme tokens applied (1 of 9 KeyNova presets per booking-engine-theme skill), 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-key which 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.tsx collects (checkIn, checkOut, adults, children, rooms, promoCode?). hmp-booking-engine/src/hooks/useAvailability.ts (TanStack Query, 2-min staleTime) POSTs to /api/booking-engine/search/availability (or GETs /booking-engine/availability for the legacy path; both honour @SetMetadata(REQUIRE_API_KEY, true) per W0.2.2 2026-05-06). Gateway routes via hmp-api-gateway/src/config/services.config.ts:1257 (/booking-engine prefix) → 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 calls availability.service.ts::checkAvailability() which derives effectiveCapacity = totalRooms + bufferRooms (P0-2 overbook buffer) minus existing RoomReservation rows 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-key header 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 staleTime together create the D-002 from EP-011 race: two browsers on a peak night (Diwali / NYE) both see availableCount=1 and both proceed to checkout. The BookingTrip multi-leg cart added expiresAt = now + 10min 2026-05-19 for chain hub trips, but single-property search has no equivalent hold. SME-PROPOSED: surface "Reserved for 10 minutes" badge + POST /availability/hold on 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 <= 3 triggers urgency badge) + RatePlanSelector.tsx. Rate plan fetch via GET /booking-engine/rate-plans/:propertyId (hmp-reservation-service/src/booking-engine/booking-engine.controller.ts:103) which aggregates from hmp-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-side isRecommended flag — EP-011 D-003 SME-PROPOSED).
  • Notes: GST display is the most expensive bug in the funnel: BookingSummary.tsx hard-codes subtotal * 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's TaxConfiguration (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) is Not 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 in src/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+ :: validatePromoCode specs — 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.tsx collects { firstName, lastName, email, phone, idType?, idNumber?, gstin?, specialRequests? }. Returning-guest pre-fill: on email blur, calls hmp-reservation-service/src/booking-engine/booking-engine.service.ts::lookupGuestByEmail() which proxies to hmp-guest-service GuestServiceClient.findByEmail(propertyId, email) (5-min cache). DTO validation: hmp-reservation-service/src/booking-engine/dto/booking-engine.dto.ts::CreateBookingDto. Aadhaar masking for display in GuestForm.tsx; Verhoeff validator lives in hmp-guest-service/src/guests/guests.service.ts for 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 consentGivenForFullAadhaar checkbox + no idHash (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-004 dpdpa-children-consent.flow.spec.ts requires 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.tsx render Σ perNightRate × nights + taxes - discount. Backend pricing preview: hmp-reservation-service/src/reservations/reservations.service.ts::preview() (called via POST /booking-engine/preview-pricing) which invokes hmp-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:
    1. GST slab (EP-011 D-003 P0): hard-coded 12 % vs per-slab {0%, 12%, 18%}. Drives the wrong total → wrong Razorpay charge.
    2. CGST/SGST vs IGST split: today only a single gstAmount is 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).
    3. Resort fees / property fee: Indian-hospitality reality has Property.dailyResortFee and Property.cleaningFee as per-night additions in some chains. Today the booking-engine does not surface these as line items.
  • Story: US-011-004 (Razorpay Payment Integration) + US-015-001 (Razorpay Payment Gateway Integration).
  • Code: hmp-booking-engine/src/components/booking/PaymentMethods.tsx lets guest pick UPI / card / netbanking / wallet. hmp-booking-engine/src/components/PaymentForm.tsx orchestrates create-reservation → initiate-payment. Reservation create (idempotent): POST /booking-engine/reservations (booking-engine.controller.ts — API-key gated @Public) → booking-engine.service.ts::createBooking() (calls GuestServiceClient.findOrCreateGuestByEmail then ReservationsService.create() with source='BOOKING_ENGINE' + idempotency-key from Idempotency-Key header 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 to PaymentServiceClient.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 via hmp-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-engine has 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 per MerchantAccount row); Tenant.isDemoTenant honoured (demo-tenant guard rejects rzp_live_* per hmp-payment-service/src/demo-tenant/demo-tenant.service.ts — DEMO_TENANT_REQUIRES_SANDBOX_KEY).
  • Post-conditions: Razorpay Order exists (gatewayOrderId returned). Local Payment row in payment-service Postgres with status='INITIATED'. The booking-engine SPA holds (razorpayOrderId, razorpayKeyId) and opens the hosted Checkout modal.
  • Notes: EP-011 D-005 (P0) — VITE_FEATURE_PAYMENT_SERVICE_ENABLED defaults OFF in production per platform CLAUDE.md. The flag-OFF legacy path goes through /booking-engine/payments/initiate (NOT -v2) which trusts client-supplied amount — tamper-risk. The flag-ON proxy path re-derives amount from reservation.totalAmount server-side — secure. Until the flag flip ships, the secure code path is unreachable in prod. AP-T12-004 PaymentReconcile cron sweeps stuck INITIATED payments 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:
    1. Redirect (synchronous) — Razorpay redirects browser to the SPA's success URL with (razorpay_payment_id, razorpay_order_id, razorpay_signature). SPA posts these to POST /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.ts POST /payments/:id/verify-razorpay-redirect → razorpay-signature.ts HMAC-SHA256 check.
    2. Webhook (asynchronous, authoritative) — Razorpay POSTs payment.captured (or payment.failed) to POST /api/webhooks/razorpay-gateway/:merchantAccountId (hmp-payment-service/src/webhooks/webhook.controller.ts — @Public, raw body via body-parser.raw in main.ts). webhook-dispatcher.service.ts::verifyAndParseWebhook() checks HMAC + dedups. On capture, payment-event-publisher.service.ts emits payment.captured via the outbox → hmp-frontdesk-service PaymentCapturedConsumer (20/20 specs) writes a folio PAYMENT transaction with dedup key folio:{folioId}:payment-captured:{paymentId} and flips Reservation.paymentStatus='PAID' + Reservation.status='CONFIRMED'. Reconcile cron: webhook-reconcile.service.ts requeues stuck RECEIVED events > 5 min old.
  • 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.balanceDue reduced by Payment.amount (Indian INR-in-paise round-trip via @hmp/payments::toPaise/fromPaise). Outbox event RESERVATION_CONFIRMED queued.
  • 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. PaymentRefundedConsumer is [SME-PROPOSED]. Razorpay paise round-trip (gotcha 8): the toPaise/fromPaise helper exists in @hmp/payments but 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 a ReservationHistory row and emits reservation.created via Kafka (Redis Streams in dev). When the payment.captured webhook lands (Step 8), PaymentCapturedConsumer patches Reservation.paymentStatus='PAID' and (if the rate plan requires payment-on-arrival vs payment-now) flips status='CONFIRMED'. The transactional outbox writes the RESERVATION_CONFIRMED event in the same DB transaction as the status flip; a background dispatcher publishes to Kafka with at-least-once delivery. Confirmation number generated by generateConfirmationNumber(prefix) — prefix defaults to RES (NOT Property.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" asserts status === '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 CONFIRMED immediately on create, no payment required).
  • Post-conditions: Reservation.status='CONFIRMED', confirmationNumber populated, source='BOOKING_ENGINE', ReservationHistory[0] row written, Folio row exists in reservation-service read model AND in frontdesk-service write model. Outbox publishes RESERVATION_CONFIRMED event.
  • Notes: EP-011 US-011-006 / D-007 flagged a known cross-service bug: in some compose environments GuestServiceClient.findOrCreateGuestByEmail returns a guest ID that doesn't yet exist in reservation-service's local guests read-model → reservations_guestId_fkey violation. The flow-3-booking-engine.spec.ts S2 fallback path documents this as a "Phase 2 deferred" cross-service migration item. Until fixed, S2 falls back to the JWT-authenticated /api/reservations path with source='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-305 POST /booking-engine/notifications/booking-confirmation → booking-engine.service.ts::sendBookingConfirmation() publishes a Kafka event consumed by hmp-notification-service which renders via hmp-shared-packages/packages/template-engine (MJML block-tree → HTML for email; @hmp/notification-contracts per-channel __fallback strings 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.tsx render the in-browser confirmation while the email is in flight. PDF voucher generator lives in hmp-frontdesk-service invoice-pdf module (not enumerated yet for booking-engine path — see Notes).
  • Test: hmp-reservation-service/src/booking-engine/booking-engine.service.spec.ts :: sendBookingConfirmation Kafka 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.ts verifies confirmation page renders post-payment.
  • Pre-conditions: Step 9 completed (RESERVATION_CONFIRMED event 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:
    1. Reservation.guestLanguage not persisted (EP-011 D-012 P1): the post-checkout chain (check-in reminder T-24h, post-stay survey) regresses to property's defaultLocale because the booking-time locale isn't carried forward. SE Asia (th/vi) guests get English follow-ups.
    2. 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.
    3. 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 single gstAmount — 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.tsx calls GET /api/reservations?propertyId=…&fromDate=…&toDate=… → hmp-reservation-service/src/reservations/reservations.controller.ts::search() (POST /reservations/search with SearchReservationsDto; 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-room writes RoomReservation.roomId and emits room.assigned + reservation.modified with changeType='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 (with pollUntil to 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 / ADMIN role) 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) — /arrivals exists 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 in flow-3.spec.ts S5 via the pollUntil workaround.

Failure modes worth testing​

#ScenarioExpected behaviourCoverage todayOwning story
F1Payment failed (insufficient funds / card declined)Razorpay payment.failed webhook; reservation stays PENDING; SPA shows error; guest can retryhmp-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
F2Razorpay signature invalid (tampering attempt)verifyPayment() returns {success:false}; reservation status UNCHANGED (W1.A.2 fail-closed contract); no folio mutationflow-3-booking-engine.spec.ts :: S4 — PASS (security contract verified)US-011-004
F3Duplicate booking submission (browser double-click)Idempotency-Key header per AP-T03-001 returns the same reservation on replay; no second Reservation rowidempotency-replay.flow.spec.ts — exercises POST /reservations replayUS-003-001
F4Inventory race — two guests both see availableCount=1, both submitFirst 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 patternUS-011-001 / US-003-006 / US-003-007
F5Abandoned 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/+72hNONE today — EP-011 D-011 P1 [SME-PROPOSED] US-011-NEW-ABANDONED-CARTUS-011-008 / new
F6OTA double-booking (channel push + direct booking same inventory)First write wins; channel-service emits channel.sync.failed event; revenue manager alertedchannel-booking-received.consumer.spec.ts covers the consumer side. Cross-channel race NOT covered.EP-009 (channel) / US-003-006
F7Promo code exhausted between apply-time and booking-timeToday: silently applies discount; user gets unauthorized discount. SME-PROPOSED: re-validate at createBooking()NONE — EP-011 US-011-008 SME-PROPOSEDUS-011-008
F8Cross-tenant guest fetch (attacker passes another tenant's guest email to BE API)403 TENANT_MISMATCH from assertPropertyTenant() in receiverguest-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
F9Demo-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 — PASSUS-015-001
F10Guest under 18 books without guardian consentToday: booking-engine GuestForm doesn't enforce; backend accepts. SME-PROPOSED: front-end gate per EP-004 US-004-012 + DPDPA §9dpdpa-children-consent.flow.spec.ts covers backend; front-end gapEP-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.ai per 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 uses mail.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 __fallback strings but delivery requires the approved template ID.
  • Razorpay merchant onboarding — smoke-merchant-account.real.spec.ts covers UI; full saga in uc-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 isRecommended only. EP-011 US-011-002 SME-PROPOSED: tenant-managed isRecommended boolean 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.html for 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.

IDTypeSeverityDescriptionOwning EPIC driftSurfaced by step
BF-002-D-001India-hospitality + tests-assert-behaviour-story-doesnt-describeP0BookingSummary.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-003Steps 3, 6
BF-002-D-002Catalog-honest + tampering-riskP0VITE_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-005Step 7
BF-002-D-003Story-vs-code + DPDPA + revenue-leakP0cancelBooking() 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-009Step 8 (cancellation symmetry)
BF-002-D-004Story-vs-code + DPDPA + Aadhaar Act §29(2)P1Aadhaar 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-004Step 5
BF-002-D-005Test-vs-code + missing-coverageP1PaymentForm.tsx, PaymentMethods.tsx, useRazorpayCheckout.ts have zero unit specs despite owning the highest-error-rate step in the funnel.EP-011 D-006Step 7
BF-002-D-006Story-incomplete + abandoned-cart-recoveryP1No 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-011Step 4 / F5
BF-002-D-007Story-incomplete + India-hospitality-critical + i18nP1Reservation.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-012Step 10
BF-002-D-008Cross-EPIC-handoff + missing-testP1/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-009Step 11
BF-002-D-009Story-incomplete + India-hospitality-criticalP1Inter-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-010Catalog-gap + missing-storyP1"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-013Step 7 (price recompute)
BF-002-D-011Cross-service-handoff + known-bugP2flow-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-012Story-example-misleadingP2Story 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-011Step 9

Cross-EPIC narrative summary​

StepSpansWhy cross-EPIC
1EP-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 → 4EP-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.
5EP-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.
6EP-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 → 8EP-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 → 10EP-003 ↔ EP-012E (notifications)Reservation confirmation event → notification-service → SES email + WhatsApp + SMS. Template engine + per-event Zod schemas owned by EP-012E.
11EP-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.