Skip to main content

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 BookingFlowStep1 either via J-02 handoff or direct subdomain entry.

3. Entry Points

#EntryNotes
1Continuation from J-02 with criteria pre-filledDefault
2Direct 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 /transliterate returns 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 IdTypeSelect rules.
  • 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" and BidiText wrapper 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

OperationEndpointIdempotencyNotes
searchAvailabilityPOST /api/v1/booking/availabilityn/aCached for 30 s per criteria
createHoldPOST /api/v1/booking/holdX-Idempotency-KeyTTL = 8 min; renewed on extras change
validateGuestPOST /api/v1/booking/validateX-Idempotency-KeyReturns transliteration suggestions if AI on
confirmBooking (cash)POST /api/v1/booking/confirmX-Idempotency-KeyNo payment auth; persists with paymentStatus = "PendingCashOnArrival"
getReservationGET /api/v1/reservations/:idn/aPolled until confirmed
transliterate (AI)POST /api/v1/ai/transliteraten/aProvenance 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 bookingDraft keyed by holdId in 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).
  • confirm keys are unique per confirmation attempt (replay returns same reservation).

7. AI Behavior

SurfaceStepPurposeModel classEdge / CloudHITL UIProvenance UIFallback
AiTransliterationCardStep 3 (Guest info)Transliterate non-Latin name to Latin formsmall text-to-textCloud (Phase 1); edge ONNX (Phase 2)Canonical card with Accept / Modify / RejectPill: model@version, prompt, traceManual 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

ErrorTriggerUX shownRecoveryTelemetry
BOOKING_HOLD_TTL_EXPIRED8-min TTL elapsedInline banner: "Your hold expired - we'll re-check availability"; auto-rerun availabilityNew hold issued silently if room still availablefrontend.booking.hold_expired { holdId }
BOOKING_CONFLICTHold lost to concurrent bookerModal: "Last room just booked - choose another room"; routes back to Step 1User selects alternative; AI may suggest alternates (P2)frontend.booking.conflict { roomTypeId }
BOOKING_VALIDATION_FAILEDServer-side guest validation failureInline field errors; focus jumps to first invalid fieldUser correctsfrontend.booking.validation_failed { fields }
MELMASTOON.AI.TRANSLITERATION_TIMEOUTAI > 1 s budgetField works without suggestion; helper text: "AI suggestion unavailable"Manual entryerror.surfaced { code }
BOOKING_RATE_PRICING_DRIFTSnapshot mismatchBanner: "Prices updated - please review"; total refreshedUser can continue or cancelfrontend.booking.price_drift { delta }
BOOKING_TENANT_DISABLED_CASH_MIDFLOWTenant policy changed during flowBanner: "Cash on arrival no longer available - please use card"; auto-route to J-04 step 4 with card pre-selectedUser pays by card or abandonsfrontend.booking.cash_disabled_midflow

10. E2E Test Gates

  • Composite gate G-WEB-1 step "5-step booking flow (cash) -> confirmation".
  • Composite gate G-MOB-1 step "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

MetricTarget
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-describedby for 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_toggled
  • frontend.booking.total_updated
  • frontend.booking.transliteration_suggested / _action
  • frontend.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.v1
  • melmastoon.booking.created.v1 (with paymentMethod="cash", paymentStatus="PendingCashOnArrival")
  • melmastoon.notifications.confirmation.sent.v1
  • melmastoon.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.

References