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​