Skip to main content

13 — Tenant Booking Web Specification

Surface: Tenant Booking Web (Next.js 14+ App Router, multi-tenant themed) BFF: bff-tenant-booking-service Audience: Guests who have selected a specific property and are completing the booking Runtime: Browser; same bundle as Consumer Meta Web but rendered under tenant theme context


1. Overview

The Tenant Booking Web is a single shared Next.js codebase that renders the booking funnel for every tenant, with each tenant's brand applied at runtime via theme-config-service. The guest reaches this surface either via:

  1. Handoff from the Consumer Meta Web (J-02) — same browser session, context switch.
  2. Direct URL (<tenantSlug>.melmastoon.app/book or melmastoon.app/<tenantSlug>/book) — deep link or marketing.
  3. OTA/partner redirect (future, R3).

Design constraint: The funnel is constrained — tenants configure toggles and content, but cannot change the funnel's step order, payment architecture, or accessibility properties. This keeps the experience consistent and secure across all tenants.


2. Tech stack

Same as 12-meta-web-specification.md with additions:

ConcernChoice
Theme injectionSSR — CSS variables injected into <head> from bff-tenant-booking-service /bootstrap
State — booking draftZustand + MMKV-compatible persistence (survives page refresh)
PaymentAdyen Web Drop-in (card, Apple/Google Pay, local methods) + Cash-on-Arrival (custom component)
IdempotencyX-Idempotency-Key: <ulid> on all state-changing BFF calls
Hold timerReact countdown component (BFF-issued hold TTL)
FormReact Hook Form + Zod (C7)

3. Booking funnel — step model

[Room selection] → [Rate plan] → [Guest details] → [Payment] → [Confirmation]
↕ (can go back to any prior step)

Each step is a distinct URL:

/<locale>/<tenantSlug>/book/<propertyId>/rooms
/<locale>/<tenantSlug>/book/<propertyId>/rates?room=<roomTypeId>
/<locale>/<tenantSlug>/book/<propertyId>/guest-details?hold=<holdId>
/<locale>/<tenantSlug>/book/<propertyId>/payment?hold=<holdId>
/<locale>/<tenantSlug>/book/<propertyId>/confirmation?res=<reservationId>

Hold is acquired after room + rate selection and before guest-details:

POST /tenant-booking/v1/holds → holdId + expiresAt

Hold TTL: 15 minutes (configurable per booking-hold-timeout-minutes feature flag). A hold timer countdown renders in the funnel header.

3.1 Step 1 — Room selection

Content:

  • Tenant header (logo, nav).
  • Property info bar (name, star rating, check-in/out dates from URL).
  • Room type cards, each showing: photo, name, capacity, key amenities, rate-from per night, availability.
  • "Select" button on each card → moves to rate selection.
  • Date / guest adjustment affordance (modify search without leaving funnel — reruns availability query).

Data: GET /tenant-booking/v1/availability?propertyId=&from=&to=&adults=&rooms=.

3.2 Step 2 — Rate plan selection

Content:

  • Selected room type summary.
  • Rate plan cards: name, description, cancellation policy badge (green=free, amber=partial, red=non-refundable), price per night + total for stay.
  • Price breakdown on hover/tap: base rate × nights + taxes.
  • Hold acquired on "Select rate": POST /tenant-booking/v1/holds.

FX display: If guest's browser locale differs from property currency, show "≈ USD X,XXX" alongside AFN. FX snapshot from pricing-service.

3.3 Step 3 — Guest details

Fields (see C7 §5.1 for full field spec):

  • Full name, email, phone, nationality.
  • Check-in time estimate.
  • Special requests (textarea).
  • Child ages (if children > 0 and tenant.booking-flow.child-policy enabled).
  • Company name / VAT (if tenant.booking-flow.company-booking enabled).
  • Loyalty / promo code (if tenant.booking-flow.loyalty-code enabled).

Validation: Zod schema from @ghasi/schemas/booking. Client-side + server-side merge (C7 §3.3).

3.4 Step 4 — Payment

Payment method selection (tenant-configurable set):

  1. Cash on arrival — shows informational card (no card details required). Guest confirms amount to be paid at property.
  2. Card (Adyen Drop-in) — PCI-compliant Adyen iframe; tenant's Adyen account configured in payment-gateway-service. 3DS2 flow if required.
  3. Mobile money (feature flag booking-mobile-money) — Adyen local method or direct MFS API.

Hold timer — visible countdown from hold expiresAt. On expiry: show ER-05 modal "Hold expired — start over".

FX snapshot — if currency differs from display: "Rate locked at AFN X = USD Y as of HH:MM" with lock icon.

Submit: POST /tenant-booking/v1/reservations (idempotency-keyed). On success → confirmation step.

3.5 Step 5 — Confirmation

Content:

  • Success illustration (animated check).
  • Reservation reference (large, copyable).
  • Booking summary: dates, room, rate, total, payment method.
  • Pre-arrival info: check-in time, property address, directions link.
  • "Add to calendar" (ICS download).
  • "Download receipt" (PD-01 PDF via reporting-service).
  • "View your booking" → guest account bookings page (requires login or guest token).
  • "Book another room" → back to property detail.

Email: notification-service sends EM-01 confirmation email immediately after reservation.confirmed.v1 event.


4. Tenant theming

At bootstrap, the BFF returns the tenant's ThemeVersion as CSS variables injected into <head>:

<style>
:root {
--color-primary: #2563eb;
--color-primaryHover: #1d4ed8;
/* ... all tokens ... */
--font-family-heading: 'Amiri', serif;
/* ... */
}
</style>

The funnel components use only token variables — no hardcoded colors. Theming is transparent to component code.

Loading state during bootstrap: Show a skeleton funnel with neutral tokens until the tenant theme resolves (< 200 ms in normal conditions). Prevents flash of wrong brand.


5. Multi-currency / FX handling

  • Rates are stored and settled in tenant.billingCurrency (e.g., AFN).
  • Display currency may differ (guest's browser locale preference).
  • FX rates are fetched at quote time from pricing-service and snapshotted at hold acquisition.
  • The snapshot AFN→display-currency rate is locked for the duration of the hold.
  • On confirmation, the receipt shows both AFN and the guest's display currency.
  • The booking contract is always in AFN (or tenant billing currency); the display currency is informational only.

6. Offline and degraded behavior

ScenarioBehavior
Network lost before step 3Block advance; show C3 ER-07 offline banner; last-fetched availability cached
Network lost during payment intent creationToast + retry button; do not re-submit without idempotency key
Adyen Drop-in fails to loadShow fallback "Cash on arrival" option + inline banner "Card payment temporarily unavailable"
Hold expires offlineOn reconnect: re-check hold validity; if expired, show ER-05

7. Funnel analytics

Booking funnel events from C1:

EventWhen
mel.booking.funnel.step_viewedEach step mounts
mel.booking.funnel.step_completedUser advances
mel.booking.funnel.step_abandonedUser exits without completing
mel.booking.payment.3ds_started3DS challenge opens
mel.booking.confirmation.shownConfirmation renders

Funnel conversion = steps 1 → 5 completion rate. Target: > 60% of holds confirmed.


8. Performance budget

See ../common/09-non-functional-requirements.md §1.1 for canonical web targets.

Additional booking-specific targets:

  • Bootstrap (theme + availability) to first step rendered: < 1.2 s p95
  • Quote response: < 800 ms p95
  • Hold acquisition: < 600 ms p95
  • Payment confirmation: < 3 s p95

9. Accessibility

  • Funnel step indicator: aria-label="Step N of 5: <name>".
  • Hold timer: aria-live="polite" region announces "5 minutes remaining".
  • Payment iframe: accessible label for Adyen iframe.
  • Error states: all errors role="alert".
  • Progress is not lost on Escape (Escape is for dialogs only).

10. Open Questions

  • Should step URLs be crawled by search engines? Leaning no (funnel steps are session-bound; robot.txt disallow /book/).
  • For cash-on-arrival: should we show a "deposit required" field if the tenant has configured a minimum deposit?
  • Hold TTL: 15 minutes is the default — does this need to be shorter for high-demand periods (e.g., Nowruz)?

References