J-03 — Multi-Step Booking with Cash on Arrival
One-liner: A guest completes a 5-step booking on a tenant site choosing cash on arrival, confirms the reservation, and walks away with a confirmation code, voucher PDF, and SMS notice.
1. Purpose
Guest completes a multi-step booking on the tenant site, picks cash on arrival as the payment option, and exits with a reservation in confirmed state, a confirmation code, a voucher PDF, and an SMS confirmation. Outcome: a holdable, cancellable reservation with no funds yet captured; merchant agreement honoured (cash policy explicit).
2. Persona Context
- Persona: Guest.
- Surfaces: Tenant Booking Web (
<tenantSlug>.melmastoon.app), Tenant Booking Mobile (deep-linked from consumer app or app-link). - Primary BFF:
bff-tenant-booking-service. - Backing services:
booking-service,pricing-service,policy-service,notification-service,ai-orchestrator-service(transliteration),audit-service. - Preconditions: Tenant has at least one published rate plan that includes a cash-on-arrival option; tenant cancellation policy is published.
- Trigger: Guest lands on
BookingFlowStep1either via J-02 handoff or direct subdomain entry.
3. Entry Points
| # | Entry | Notes |
|---|---|---|
| 1 | Continuation from J-02 with criteria pre-filled | Default |
| 2 | Direct subdomain entry, then "Find rooms" | Bootstrap runs without pre-fill |
| 3 | "Re-book" CTA in My Trips (Phase 2) | Pre-fills criteria from a past stay |
4. Screen-by-Screen Flow
4.1 BookingFlowStep1RoomTypeSelection
- Layout: List of room types (cards with photo, capacity, base rate per night, "What's included" toggle), filter for amenity (king bed, sea view, etc.), persistent rate-summary chip in header showing total nights + adults/rooms.
- Components:
RoomTypeListCard,WhatsIncludedDrawer,OccupancyEditor,Pagination. - Offline: Read-only from PWA cache; "Continue" disabled until online.
- AI: None.
- Errors: No availability for selected dates -> empty state with "Try other dates" + Phase-2 alternative-date suggestions.
- Loading: Skeleton cards mirror real layout; LCP element = first room photo.
- A11y: Each card is a tab stop with full aria-label; "Add to room" buttons have explicit verb names.
- RTL: Card meta row mirrored; "from
/night" copy follows reading direction. - Perf: Mounts <= 1.5 s p95 on cold mobile; subsequent step transitions <= 200 ms.
- Telemetry:
frontend.booking.step_viewed { step: 1 };frontend.booking.room_type_selected { roomTypeId }.
4.2 BookingFlowStep2ExtrasAndPolicies
- Layout: Optional extras grouped by category (Breakfast, Halal-meal options, Airport pickup, Late checkout); cancellation/no-show policy and ID-on-arrival requirements explicit; running total updates instantly.
- Components:
ExtraGroup,PolicyAccordion,PriceBreakdownCard. - Offline: Read-only; "Continue" disabled.
- AI: None.
- Errors: Extra unavailable for selected dates -> disable + tooltip; total mismatch with server -> auto-refresh with banner "Prices updated".
- Loading: Inline shimmer on price refresh.
- A11y: Each extra is a checkbox with explicit label including price impact; total announced via
aria-live="polite"on change. - RTL: Quantity steppers mirror; price values stay LTR (numerals follow tenant numerals rule).
- Perf: Re-price on extras change <= 300 ms p95.
- Telemetry:
frontend.booking.extra_toggled { extraId, on/off };frontend.booking.total_updated { amount, currency }.
4.3 BookingFlowStep3GuestInformation
- Layout: Two-column form on desktop / single column on mobile: legal first name, last name, email, phone (with country code), nationality, ID type, ID number (collected at this step or deferred to check-in per tenant policy), special requests (free-text).
- Components:
Form(RHF + Zod),PhoneInput,LocaleSelect,IdTypeSelect,BidiText,AiTransliterationCard. - Offline: Form is fully usable; "Continue" disabled until online to call
/booking/validate. - AI: When user types name in non-Latin script,
ai-orchestrator-service /transliteratereturns Latin variants with confidence and alternatives. UX is the canonical HITL card: shown beside the field, never auto-applied; provenance pill visible. - Errors: Validation errors per field; phone format coerced; ID number format validated by
IdTypeSelectrules. - Loading: Submit button shows spinner; transliteration suggestions stream in.
- A11y: Every field has visible label; inline errors live in
aria-describedby; transliteration suggestion is announced and reachable via Tab. - RTL: Form labels and helper text mirror; numerals localised; LTR-locked fields (email, IDs) annotated with
dir="ltr"andBidiTextwrapper to preserve bidi. - Perf: Form interaction INP < 200 ms; transliteration round-trip <= 1 s p95.
- Telemetry:
frontend.booking.transliteration_suggested { fieldId, model };frontend.booking.transliteration_action { action: accept|modify|reject }.
4.4 BookingFlowStep4ReviewAndPayment
- Layout: Read-only review block (room, dates, extras, guests, totals, taxes), payment-method selector with Cash on Arrival vs Card; selecting Cash shows the cash policy text and prepaid-deposit rule (if any per policy).
- Components:
BookingReviewCard,PaymentMethodPicker,PolicyHighlightCard,Button(variant=emphasis"Confirm booking"). - Offline: Read-only; "Confirm booking" disabled until online.
- AI: None.
- Errors: Payment-method validation; if cash policy disabled mid-flow -> banner "Cash on arrival not available - please use card or contact hotel".
- Loading: Confirm CTA spinner; transition <= 2 s p95.
- A11y: Payment method radios are a labelled group; policy text expands inline (
aria-expanded); confirm button announces enable/disable state changes. - RTL: Layout mirrors; numbers stay LTR (Latin numerals if tenant policy = Latin).
- Perf: Final confirm RTT <= 2 s p95 (hold + write happens server-side; cash path skips card auth).
- Telemetry:
frontend.booking.payment_method_selected { method: cash };frontend.booking.confirm_clicked { idempotencyKey }.
4.5 BookingFlowStep5ConfirmationScreen
- Layout: Confetti-free success card with: confirmation code (large, copyable), summary block, voucher PDF download, SMS sent confirmation, "Add to Apple Wallet / Google Wallet" buttons (Phase 2), "Open in Maps" link, contact info for the property.
- Components:
ConfirmationCard,VoucherDownloadButton,WalletPassButton(P2),MapPreviewLink. - Offline: Reachable from local cache (saved on success); voucher cached locally; sharing links may be disabled offline.
- AI: None.
- Errors: Voucher generation deferred -> "Voucher will be available shortly; we'll email you" with status checker.
- Loading: Once shown, instantaneous; voucher button shows spinner during PDF generation.
- A11y: Page heading focus on confirmation code; copy-to-clipboard announces "Confirmation code copied" via
aria-live. - RTL: Layout mirrors; confirmation code is LTR + announced character-by-character to assistive tech.
- Perf: PDF generation <= 3 s p95; SMS send confirmation status <= 5 s.
- Telemetry:
frontend.booking.confirmation_viewed { reservationId };frontend.booking.voucher_downloaded.
5. State Machine
6. Data Requirements
6.1 Server state
| Operation | Endpoint | Idempotency | Notes |
|---|---|---|---|
searchAvailability | POST /api/v1/booking/availability | n/a | Cached for 30 s per criteria |
createHold | POST /api/v1/booking/hold | X-Idempotency-Key | TTL = 8 min; renewed on extras change |
validateGuest | POST /api/v1/booking/validate | X-Idempotency-Key | Returns transliteration suggestions if AI on |
confirmBooking (cash) | POST /api/v1/booking/confirm | X-Idempotency-Key | No payment auth; persists with paymentStatus = "PendingCashOnArrival" |
getReservation | GET /api/v1/reservations/:id | n/a | Polled until confirmed |
transliterate (AI) | POST /api/v1/ai/transliterate | n/a | Provenance returned in payload |
6.2 URL state
/book/:step(1..5); current step deep-linkable; refresh resumes from server-side hold + draft state.- Draft is server-side per
holdId; client never serializes PII to URL.
6.3 Local persistence
- Draft
bookingDraftkeyed byholdIdin IndexedDB / MMKV with TTL == hold TTL; cleared on confirmation. - Last 5 reservations cached for "My Trips" (Phase 2).
6.4 Idempotency
- Per-step mutations carry
X-Idempotency-Key(ULID generated client-side at step entry; reused on retry). confirmkeys are unique per confirmation attempt (replay returns same reservation).
7. AI Behavior
| Surface | Step | Purpose | Model class | Edge / Cloud | HITL UI | Provenance UI | Fallback |
|---|---|---|---|---|---|---|---|
AiTransliterationCard | Step 3 (Guest info) | Transliterate non-Latin name to Latin form | small text-to-text | Cloud (Phase 1); edge ONNX (Phase 2) | Canonical card with Accept / Modify / Reject | Pill: model@version, prompt, trace | Manual entry; user types Latin equivalent |
8. Offline Behavior
- All form steps are usable offline (local form state).
- Steps 4 (Confirm) and 5 (Confirmation) require online.
- Hold cannot be created offline (server-side resource); user sees blocking banner with "Try again when online".
- Once confirmed online, the confirmation screen is available offline from local cache.
9. Error States
| Error | Trigger | UX shown | Recovery | Telemetry |
|---|---|---|---|---|
BOOKING_HOLD_TTL_EXPIRED | 8-min TTL elapsed | Inline banner: "Your hold expired - we'll re-check availability"; auto-rerun availability | New hold issued silently if room still available | frontend.booking.hold_expired { holdId } |
BOOKING_CONFLICT | Hold lost to concurrent booker | Modal: "Last room just booked - choose another room"; routes back to Step 1 | User selects alternative; AI may suggest alternates (P2) | frontend.booking.conflict { roomTypeId } |
BOOKING_VALIDATION_FAILED | Server-side guest validation failure | Inline field errors; focus jumps to first invalid field | User corrects | frontend.booking.validation_failed { fields } |
MELMASTOON.AI.TRANSLITERATION_TIMEOUT | AI > 1 s budget | Field works without suggestion; helper text: "AI suggestion unavailable" | Manual entry | error.surfaced { code } |
BOOKING_RATE_PRICING_DRIFT | Snapshot mismatch | Banner: "Prices updated - please review"; total refreshed | User can continue or cancel | frontend.booking.price_drift { delta } |
BOOKING_TENANT_DISABLED_CASH_MIDFLOW | Tenant policy changed during flow | Banner: "Cash on arrival no longer available - please use card"; auto-route to J-04 step 4 with card pre-selected | User pays by card or abandons | frontend.booking.cash_disabled_midflow |
10. E2E Test Gates
- Composite gate
G-WEB-1step "5-step booking flow (cash) -> confirmation". - Composite gate
G-MOB-1step "5-step booking flow (cash) -> confirmation". - Hold-expiry recovery scenario.
- Transliteration AI offline-fallback scenario.
- RTL render verified for all 5 steps in Pashto.
11. Performance Requirements
| Metric | Target |
|---|---|
| Step 1 mount (cold mobile) | <= 1.5 s p95 |
| Step transitions | <= 200 ms |
| Re-price on extras change | <= 300 ms p95 |
| Confirm RTT (cash) | <= 2 s p95 |
| Voucher PDF generation | <= 3 s p95 |
| Transliteration AI round-trip | <= 1 s p95 |
12. Accessibility Requirements
- Every step is keyboard-completable end-to-end.
- Forms use RHF + Zod with
aria-describedbyfor inline errors. - Focus jumps to first invalid field on submit failure.
- Step navigation announces step number and total ("Step 3 of 5") via
aria-live. - Wallet pass buttons (P2) are keyboard-accessible.
- Contrast verified for cancellation policy emphasis text.
13. Telemetry
Frontend events
frontend.booking.step_viewed { step }frontend.booking.room_type_selected { roomTypeId }frontend.booking.extra_toggledfrontend.booking.total_updatedfrontend.booking.transliteration_suggested/_actionfrontend.booking.payment_method_selected { method }frontend.booking.confirm_clicked { idempotencyKey }frontend.booking.confirmation_viewed { reservationId }frontend.booking.hold_expired/frontend.booking.conflict/frontend.booking.price_drift
Domain events emitted
melmastoon.booking.hold.created.v1melmastoon.booking.created.v1(withpaymentMethod="cash",paymentStatus="PendingCashOnArrival")melmastoon.notifications.confirmation.sent.v1melmastoon.audit.recorded.v1
14. Success Criteria
- Confirmation screen reached <= 30 s p95 from Step 1 mount, including 5 step transitions.
- Cash-on-arrival path persists
paymentStatus = PendingCashOnArrival,paymentMethod = cash. - SMS confirmation delivered within 60 s p95 (depends on
notification-service). - PDF voucher generated within 3 s p95.
- Re-trying confirm with the same idempotency key returns the same reservationId.
- Transliteration AI suggestion is reviewable (Accept / Modify / Reject) with provenance visible.
- Hold conflicts produce the right UX and never silently overbook.