05 — Frontend Workflows and User Journeys
Status: populated Last updated: 2026-04-23 Scope: the canonical end-to-end UI workflows across every Ghasi Melmastoon surface — Consumer Meta Web, Tenant Booking Web, Consumer Mobile, Electron Desktop Backoffice, and the Control Plane. Each workflow declares its actors, role-based variants, services touched, state model, offline fallback, AI affordances, and the journey ID (
J-NN) and epic ID (EP-MEL-NN) it realizes.Companions:
../README.md·01-product-overview-frontend.md·02-architecture-overview-frontend.md·06-theming-and-tenant-config.md·03-design-system.md·04-frontend-design-guidelines.md·../desktop/21-desktop-app-specification.md·../journeys/README.md·../../07-epics-and-user-stories.md·../../04-event-driven-architecture.md·../../08-ai-architecture.md
This document is the frontend-perspective view of the platform's operational atlas. The cross-service operational atlas — events emitted, sagas, data changes per step — lives in ../journeys/README.md (22 per-journey files; the former docs/journeys/01-core-user-journeys.md is a redirect stub). This document focuses on the UI side of those journeys: which screens render, what state machines drive them, which BFF calls fire, what role variants exist, how each step degrades offline, and which AI affordances surface.
1. Critical-path workflow map
Twelve workflows cover every persona and every surface. Every workflow is an E2E gate — regression on any of them blocks release.
| ID | Workflow | Primary surface(s) | Journey link | Epic link | E2E gate |
|---|---|---|---|---|---|
| W-01 | Guest meta discovery — search, filter, compare, detail | Consumer Meta Web · Consumer Mobile (Discover tab) | J-01 | EP-MEL-02 | Yes |
| W-02 | Consumer → tenant handoff (signed token, tenant bootstrap, theme injection) | Consumer Meta → Tenant Booking | J-02 | EP-MEL-02 + EP-MEL-03 | Yes |
| W-03 | Tenant booking funnel — multi-step with cash-on-arrival | Tenant Booking Web · Consumer Mobile (Booking stack) | J-03 | EP-MEL-03 + EP-MEL-04 + EP-MEL-05 | Yes |
| W-04 | Tenant booking funnel — card payment with 3DS + FX snapshot | Tenant Booking Web · Consumer Mobile | J-04 | EP-MEL-03 + EP-MEL-04 + EP-MEL-05 | Yes |
| W-05 | Front-desk arrival — check-in, key issuance (mobile-key / PIN / encoded card) | Electron Desktop Backoffice | J-05, J-09 | EP-MEL-06 + EP-MEL-09 | Yes |
| W-06 | Stay management — modify, mid-stay issues, check-out, folio close | Electron Desktop Backoffice | J-06, J-11 | EP-MEL-06 + EP-MEL-04 | Yes |
| W-07 | Walk-in offline booking with cash deposit (full offline path) | Electron Desktop Backoffice (offline) | J-07 | EP-MEL-06 + EP-MEL-10 | Yes |
| W-08 | Housekeeping coordination + maintenance escalation | Electron Desktop Backoffice (incl. housekeeper kiosk sub-mode) · Consumer Mobile (mid-stay request) | J-19, J-20, J-21, J-22 | EP-MEL-07 + EP-MEL-08 | Yes |
| W-09 | AI-assisted operations — pricing suggestions, daily ops dashboard, anomaly inbox | Electron Desktop Backoffice | J-15, J-16 | EP-MEL-11 | Yes |
| W-10 | End-of-day cash drawer close + regulatory submission (tax / guest registration) | Electron Desktop Backoffice (print + export) | J-12, J-17, J-18 | EP-MEL-05 + EP-MEL-06 | Yes |
| W-11 | Tenant onboarding & theming — author / preview / publish / rollback | Control Plane (Phase 2) + Backoffice authoring console | J-13, J-14 | EP-MEL-01 | Yes |
| W-12 | Sync recovery & conflict resolution after offline window | Electron Desktop Backoffice (Sync Center) | cross-cutting (J-07, J-20, J-12 reconnect tail) | EP-MEL-10 | Yes |
2. Cross-workflow conventions
These apply to every workflow and are not repeated per-workflow.
- Tenant context is on every BFF call.
X-Tenant-Id(header) +tid(JWT) cross-checked at the gateway. Any mismatch surfaces a generic "session expired" page; never raw error. - Idempotency on every mutation. Every POST/PUT/PATCH carries
Idempotency-Key(ULID); user-retried clicks are no-ops, never duplicates. - Provenance on every AI step. Every AI suggestion renders
model · version · prompt · traceId · local|cloud; HITL accept on irreversible mutations. - Audit-link in every operator action. State-changing toasts on the desktop expose a "View audit" link for 10 s pointing at the audit-log row.
- Sync Status Pill is always visible on the desktop. Pending mutations and connectivity state are reflected at every step.
- Telemetry events (
frontend.<surface>.<verb>) emit per material step; backend domain events emit per service perdocs/04-event-driven-architecture.md§3. PII never enters telemetry payloads. - RTL/LTR parity is rendered for every workflow; visual regression covers both directions per primary locale.
- Locale fallback chain: requested → tenant default → platform
en-US. Missing translations never render the raw key. - Confirmation surfaces (booking confirmed, payment captured, key issued, refund approved, regulatory submission accepted) are screen-reader-readable first, printable, and locale-multilingual where applicable.
W-01 — Guest meta discovery
Actors. Guest (anonymous; returning guest with session cookie).
Surfaces. Consumer Meta Web (apps/web-meta), Consumer Mobile Discover tab (apps/mobile).
Primary BFF. bff-consumer-service.
Services touched (via BFF). search-aggregation-service · property-service · pricing-service · file-storage-service · notification-service (ratings projection).
Realizes. J-01 · EP-MEL-02.
State diagram
Step-by-step
| # | Step | UI surface | BFF call | Cacheable | Offline fallback | AI |
|---|---|---|---|---|---|---|
| 1 | Search submit (location, dates, occupancy, price band, amenities) | MetaSearchHero | POST /search | 15 s edge, query-keyed | Last-good results from PWA / MMKV cache; "Showing recent results" banner | — |
| 2 | Render results list (20 cards) | MetaResultsList | (response of step 1) | yes | Cached page render | — |
| 3 | Apply filters (halal kitchen, prayer room, …) | filter drawer (mobile) / sidebar (web) | POST /search (re-issued) | yes | Filters apply over cached set; flagged "may be incomplete" | — |
| 4 | Toggle map view | MetaResultsMap (Leaflet web / react-native-maps mobile) | POST /search/map | 15 s edge | Cached pins from last query | — |
| 5 | Open compare drawer (≤ 3) | MetaCompareDrawer | GET /search/compare?ids=… | session cache | From cached detail responses | — |
| 6 | Open property detail | PropertyDetailScreen | GET /hotels/{id} + GET /hotels/{id}/availability | 60 s edge | Last-seen detail (up to 7 days) | — |
| 7 | Save to wishlist | wishlist icon on card | POST /wishlist | no (cookie-keyed) | Mobile: queued in MMKV, replays on reconnect; web: blocked offline with banner | — |
| 8 | Tap "Book on hotel site" | listing-card CTA | → handoff (W-02 step 1) | — | — | — |
Role variants
| Persona | Variant |
|---|---|
| Anonymous guest | Default flow; wishlist persists per cookie |
| Returning guest (session cookie) | "Resume search" pill from session storage; recently-viewed strip; wishlist hydrated server-side |
| Mobile vs web | Mobile uses bottom-sheet filters and bottom-tab navigation; web uses left-rail filters and top-bar |
Offline fallback
- Web (PWA). Service worker serves the cached app shell + last successful results page from IndexedDB. New searches are queued client-side and re-run on reconnect; the user sees an "Offline — cached results" pill.
- Mobile (MMKV). Last 50 search results + last 20 viewed properties cached. Wishlist additions queued; replayed in FIFO on reconnect with idempotency keys.
- Cannot work offline. Handoff (requires server-signed token), live availability (must be authoritative).
Telemetry
frontend.meta.search_submitted · frontend.meta.results_rendered { count, latency_ms } · frontend.meta.filter_applied · frontend.meta.compare_opened { ids } · frontend.meta.property_viewed { id } · frontend.meta.handoff_initiated { tenantId, propertyId }.
W-02 — Consumer → tenant handoff
Actors. Guest (transitioning from meta to tenant context).
Surfaces. Consumer Meta → Tenant Booking Web · Consumer Mobile (Discover tab → Booking stack).
Primary BFF. bff-consumer-service (issues handoff) → bff-tenant-booking-service (consumes handoff, emits bootstrap).
Services touched. theme-config-service (active ThemeVersion) · tenant-service (status check) · pricing-service (currencies) · payment-gateway-service (enabled methods) · policies-service.
Realizes. J-02 · EP-MEL-02 + EP-MEL-03.
State diagram
Step-by-step
| # | Step | UI surface | BFF call | Notes |
|---|---|---|---|---|
| 1 | Issue signed handoff (HMAC-SHA256, single-use, 5 min TTL) | spinner overlay on listing card | POST /handoff/{tenantSlug}/{propertyId} (consumer BFF) | Body: { checkIn, checkOut, occupancy, locale, currency, sourceCampaign?, traceId } |
| 2 | Navigate to tenant URL | Web: 302 to https://<slug>.melmastoon.app/book?h=<token>; Mobile: Linking.openURL (Universal / App Link) | — | Mobile falls back to web if app not installed |
| 3 | Tenant bootstrap (SSR layout) | BootstrapTenantScreen (mobile) / Next.js layout (web) | POST /handoff/consume → GET /bootstrap | Injects theme tokens at SSR; no hydration flicker |
| 4 | Enter funnel pre-populated | first booking step (DatesScreen or RoomChoiceScreen if dates known) | (continues into W-03 or W-04) | — |
Role variants
| Scenario | Variant |
|---|---|
Direct landing (no meta, user types <slug>.melmastoon.app/book) | Same /bootstrap flow without consume; user picks dates/guests in step 1 |
| Mobile deep link | Universal Link / App Link; if app not installed → web URL fallback |
| Tenant suspended | Generic "This hotel isn't accepting direct bookings right now" page; CTA to return to meta |
| Theme version unpublished mid-deploy | Falls back to last-known-good ThemeVersion; internal alert fires to tenant GM via desktop notification feed |
Offline fallback
Online-only. Handoff requires authoritative server signature; tenant bootstrap requires authoritative theme version. If offline at the moment of handoff, the listing-card CTA is disabled with a "You're offline. Try again when connected." tooltip.
Telemetry
frontend.tenant.booking_session_started { tenantId, source } · frontend.tenant.theme_loaded { themeVersion, latency_ms } · frontend.tenant.handoff_invalid { reason }.
W-03 — Tenant booking funnel (cash-on-arrival)
Actors. Guest.
Surfaces. Tenant Booking Web (apps/web-tenant-booking) · Consumer Mobile Booking stack (apps/mobile).
Primary BFF. bff-tenant-booking-service.
Services touched (via BFF). reservation-service (quote, hold, confirm) · inventory-service (availability, allocations) · pricing-service (FX snapshot) · payment-gateway-service (cash intent) · lock-integration-service (deferred key schedule) · notification-service.
Realizes. J-03 · EP-MEL-03 + EP-MEL-04 + EP-MEL-05.
State diagram
Step-by-step
| # | Step | UI screen | BFF call | Offline fallback | AI |
|---|---|---|---|---|---|
| 1 | Select dates | DatesScreen (mobile) / inline header (web) | GET /availability (debounced) | Read-only cached availability; "Cannot book offline" banner blocks Continue | — |
| 2 | Choose room type | RoomChoiceScreen | GET /availability (cached) | Cards from cache | Recommended-room hint (opt-in) |
| 3 | Upgrades / rate plan | UpgradesScreen | (uses bootstrap rate-plan add-ons) | Read-only | — |
| 4 | Guest details (bilingual capture) | GuestDetailsScreen | PATCH /draft/{id} (debounced 500 ms) | Online-only writes (PII protection) | Transliteration suggestion (ai-orchestrator-service.transliterateName) — HITL accept/edit |
| 5 | Review (summary + holds) | ReviewScreen | POST /quote → POST /draft/{id}/hold (idempotency-keyed) | Online-only | — |
| 6 | Payment selection = Cash on Arrival | PaymentScreen | (no /payment-intent call for cash; advances to confirm) | Online-only | — |
| 7 | Confirm | ConfirmationScreen after POST /draft/{id}/confirm → GET /confirmation/{rsv} | — | — | — |
| 8 | Multilingual confirmation + voucher | ConfirmationScreen (with PDF download + Add-to-Calendar + share link) | notification-service dispatches SMS / WhatsApp / email | — | — |
Role variants
| Variant | UI difference |
|---|---|
| Single room vs multi-room | Multi-room shows occupancy stepper plus per-room guest details |
| Group booking (≥ 5 rooms) | Auto-routes to W-06 group folio path; folio is single |
| Sharia-compliant tenant | Card payment hidden; only cash-on-arrival + Hawala-mediated bank transfer surfaces |
requireGuestSignature: true | Adds signature pad before confirm (canvas web / native pad mobile) |
captureGuestPassport: true (AF tax-resident) | Passport / Tazkira fields surfaced as required in GuestDetailsScreen |
| Mobile vs web | Mobile uses vertical-stepper (one screen per step); web defaults to vertical-stepper but supports single-page preset per theme.layoutPresets.booking |
Hold-expiry handling
bootstrap.flowConfig.holdTtlSeconds drives a per-mount countdown. T-60 s: non-blocking toast warning. T-0: HoldExpiredBanner surfaces; "Re-quote with current availability" restarts at step 2. Idempotency keys ensure double-click on Confirm doesn't double-book.
Offline fallback
Online-only. The funnel is gated on authoritative inventory + pricing + payment-method state. Each step disables its CTA with a banner when offline. Already-rendered cached availability is shown as read-only context but cannot be acted on.
Telemetry
frontend.booking.quote_requested · frontend.booking.quote_received { latency_ms } · frontend.booking.method_picked { method: "cash_on_arrival" } · frontend.booking.confirm_clicked · frontend.booking.confirmed { reservationId } · KPI: direct-booking share, meta→booking conversion, time-to-confirm p95.
W-04 — Tenant booking funnel (card payment + 3DS)
Actors. Guest.
Surfaces. Tenant Booking Web · Consumer Mobile.
Primary BFF. bff-tenant-booking-service.
Services touched. reservation-service · inventory-service · pricing-service (FX snapshot) · payment-gateway-service (card intent + 3DS + capture) · lock-integration-service · notification-service.
Realizes. J-04 · EP-MEL-03 + EP-MEL-05.
Sequence (frontend perspective)
UI rules
- Total displayed twice on
PaymentScreen: AFN primary (settlement), USD secondary (card billing currency); FX-snapshot tooltip showsprovider · capturedAt · ttlExpiresAt. - Stripe Elements iframe is the only place card data is entered; the SPA never sees PAN.
- 3DS modal is the issuer's; the SPA only handles the post-redirect
redirectResulttoken viaPOST /draft/{id}/return. - Idempotent
/return— repeated calls returnkind: 'already_confirmed'. Refresh after 3DS does not double-charge. - FX-snapshot is frozen at confirm. What the user saw on
PaymentScreenis what is captured;ConfirmationScreenre-renders the same snapshot.
Failure paths
| Failure | UI behaviour | Recovery |
|---|---|---|
| Card declined | Issuer code mapped to plain language ("Your card was declined. Try another card or pay cash on arrival.") | Hold remains valid until TTL; user retries or switches to cash |
| 3DS abandoned | Banner: "Authentication not completed" | Retry from PaymentScreen |
| Stripe webhook delayed | Optimistic "Confirming…" with skeleton; if > 10 s, escalate to "Still confirming — we'll email you when done" | Background poll on GET /confirmation/{rsv} |
| FX snapshot stale before confirm | Non-blocking banner "Prices updated — refresh quote?" | Refresh re-quotes; previously held price honored if hold still valid |
Webhook fires "captured" but /return fails | Audit shows captured payment without confirmed reservation; ops alert via audit-service | Saga compensation: refund or manual reconciliation |
Offline fallback
Online-only. All steps require authoritative payment-gateway state.
Telemetry
frontend.booking.payment_method_picked { method: "card" } · frontend.booking.3ds_started · frontend.booking.3ds_completed { latency_ms } · frontend.booking.payment_captured · frontend.booking.confirmed { reservationId }.
W-05 — Front-desk arrival (check-in + key issuance)
Actors. Front-Desk Clerk (primary), General Manager (override authority), Guest (presents at desk).
Surfaces. Electron Desktop Backoffice (apps/desktop-backoffice).
Primary BFF. bff-backoffice-service.
Services touched. reservation-service · billing-service (folio open / charge / settle) · payment-gateway-service · lock-integration-service (mobile-key invite / PIN issue / encoded card) · notification-service · audit-service.
Realizes. J-05, J-09 · EP-MEL-06 + EP-MEL-09.
State diagram
Step-by-step
| # | Step | UI surface | BFF call | Offline fallback | AI |
|---|---|---|---|---|---|
| 1 | Open arrivals grid (today) | ArrivalsToday module | GET /arrivals/today | Reads from local SQLite; sync pill shows last sync time | Recommended-arrival-order hint |
| 2 | Select reservation (scan QR / search / pick) | row-action on grid | GET /reservations/{id} | Reads from local SQLite | — |
| 3 | Verify identity (passport / Tazkira / NID) | IdentityVerifyDialog (capture document, optional photo) | PATCH /reservations/{id}/identity | Captured to local outbox; replays on reconnect | OCR document scan (edge ONNX) |
| 4 | Open folio | FolioPanel | POST /folios/{rsvId}/open | Local outbox | — |
| 5 | Pre-auth incidentals (card on file) | inline action on folio | POST /folios/{id}/preauth | Online-only (vendor authoritative) | — |
| 6 | Confirm room assignment (or reassign) | RoomAssignmentPanel | POST /reservations/{id}/assign | Local outbox; conflict on reconnect → Sync Center | Optimal-room suggestion (proximity, party size, accessibility) |
| 7 | Issue key — mobile-key | KeyIssuanceWizard (vendor: mobile-key) | POST /key-credentials (vendor adapter) | Local fallback to PIN if vendor offline; mobile-key issued on reconnect | — |
| 7a | Issue key — encoded card | wizard step (USB/serial encoder) | POST /key-credentials | Local issuance via encoder driver (no internet needed for encoder itself) | — |
| 7b | Issue key — PIN | wizard step | POST /key-credentials | Local PIN generation; pushed to lock on reconnect | — |
| 8 | Welcome message dispatch | toast + inbox entry | notification-service outbound | Queued in outbox | Optional draft welcome (HITL) |
| 9 | Mark checked-in | reservation status pill flips | POST /reservations/{id}/check-in | Local outbox; status reflected immediately | — |
Role variants
| Persona | Variant |
|---|---|
| Front-Desk Clerk | Full check-in + key issuance; folio charges within scope; refund requires GM |
| General Manager | All Front-Desk + override authority (rate, refund, room assignment outside ABAC) |
| Owner (read-only) | Reads arrivals/in-house; cannot mutate |
| Compliance Officer | Reads identity verification capture for audit; cannot issue keys |
Offline fallback
Full offline. Identity verification, room assignment, PIN issuance, encoded card issuance all work offline against local SQLite + USB/serial encoder driver. Mobile-key issuance falls back to PIN when the vendor cloud is unreachable; the mobile-key credential is queued for issuance on reconnect, and the guest receives a push update at that point. Sync Status Pill always reflects the gap.
AI affordances
- Document OCR runs locally via ONNX Runtime Node on the desktop's AI worker (no PII leaves the device).
- Optimal-room suggestion considers proximity (other party rooms), accessibility flags, and noise (rooms near elevators / kitchens) — surfaced as
<AISuggestionCard />next to the room assignment panel. - Welcome message draft is HITL — clerk reviews before send.
Telemetry
frontend.desktop.arrivals_opened · frontend.desktop.checkin_started { reservationId } · frontend.desktop.identity_verified { method } · frontend.desktop.key_issued { vendor, fallback?: 'pin'|'encoded' } · frontend.desktop.checkin_completed { reservationId, latency_ms }.
W-06 — Stay management (modify, mid-stay, check-out)
Actors. Front-Desk Clerk · GM · Housekeeping Lead (informed) · Maintenance Tech (escalations) · Guest.
Surfaces. Electron Desktop Backoffice; Consumer Mobile Manage tab (read-only + mid-stay request).
Primary BFF. bff-backoffice-service (operator) · bff-tenant-booking-service (guest manage).
Services touched. reservation-service · billing-service · payment-gateway-service · housekeeping-service (touchpoints) · maintenance-service (lock battery, plumbing) · lock-integration-service (re-issue / revoke) · notification-service · audit-service.
Realizes. J-06, J-11 · EP-MEL-06 + EP-MEL-04 + EP-MEL-08.
Sub-workflows
| Sub | Trigger | Steps | Offline |
|---|---|---|---|
| Modify dates / occupancy | Guest request via desk; in-house grid action | Re-quote (POST /reservations/{id}/modify) → re-hold inventory → re-confirm; folio adjusted; FX delta surfaced explicitly | Online-only (inventory authoritative) |
| Add charge to folio (incidentals, F&B, laundry) | Folio panel "Add charge" | POST /folios/{id}/charges (idempotency-keyed) | Local outbox; replays |
| Lock battery dies mid-stay (J-09) | Lock vendor heartbeat alarm | Maintenance ticket auto-created; clerk re-issues credential (PIN fallback); maintenance escalated | Local PIN fallback works offline |
| Folio dispute / late checkout (J-11) | Manager triggers from folio panel | GM override + reason code → folio adjustment → audit event | Local outbox |
| Check-out | Folio "Check out" CTA after settlement | POST /reservations/{id}/check-out → final folio close → key revocation → housekeeping task auto-created (W-08) | Local outbox; key revocation queued |
| Revoke key | Security incident, lost mobile, dispute | KeyIssuanceWizard "Revoke" path | Local revocation written to lock vendor outbox |
Step-by-step (check-out path)
| # | Step | UI | BFF | Offline | AI |
|---|---|---|---|---|---|
| 1 | Select in-house reservation | InHouseGrid | GET /in-house | local SQLite | — |
| 2 | Open folio | FolioPanel | GET /folios/{id} | local | — |
| 3 | Settle remaining balance | SettleDialog (cash / card / refund pre-auth) | POST /folios/{id}/settle | Cash settles offline; card requires online vendor | — |
| 4 | Check out (status flip) | CheckOutAction | POST /reservations/{id}/check-out | local outbox | — |
| 5 | Auto-create housekeeping task | toast notification (links to W-08) | housekeeping-service.createTask | local outbox | Cleaning-priority hint |
| 6 | Revoke key credential | (background) | lock-integration-service.revoke | local revocation queued | — |
| 7 | Optional review prompt | notification-service outbound (T+24 h) | — | — | Auto-translated review request (HITL approved per tenant) |
Role variants
| Persona | Variant |
|---|---|
| Front-Desk | Settle, check-out within scope; refund > tenant-configured threshold requires GM |
| GM | Override on rate, refund, late-fee, dispute; reason code mandatory |
| Manager (chain) | All single-property GM scope across multiple properties via tenant switcher |
Offline fallback
Mostly offline. Cash settlement, status flips, folio adjustments, key revocation all write to the local outbox. Card settlement and lock-vendor cloud calls require connectivity (re-tried on reconnect; user is told explicitly).
Telemetry
frontend.desktop.modification_started · frontend.desktop.folio_charge_added { kind } · frontend.desktop.checkout_completed · frontend.desktop.key_revoked { reason } · frontend.desktop.dispute_overridden { reasonCode, actorRole }.
W-07 — Walk-in offline booking with cash deposit
Actors. Front-Desk Clerk · Walk-In Guest.
Surfaces. Electron Desktop Backoffice (offline mode).
Primary BFF. bff-backoffice-service (replays on reconnect).
Services touched (eventual). reservation-service · inventory-service · billing-service · payment-gateway-service (cash intent) · lock-integration-service · notification-service.
Realizes. J-07 · EP-MEL-06 + EP-MEL-10.
State diagram (offline-first)
Step-by-step (offline path)
| # | Step | UI | Local action | Sync (on reconnect) |
|---|---|---|---|---|
| 1 | Open Walk-In capture | WalkInModule | — | — |
| 2 | Verify availability | AvailabilityPicker against local inventory_local table | local read | Authoritative re-check on reconnect; conflict if double-booked |
| 3 | Pick room type, dates, occupancy | RoomChoiceLocal | local computation | — |
| 4 | Capture guest details (incl. document scan) | GuestDetailsLocal (bilingual capture; document OCR via edge ONNX) | local persist | Replays as PATCH /draft |
| 5 | Cash deposit (drawer counter capture) | CashDepositCapture (denomination breakdown, change calc) | local payment_intent (pending_cash) → captured | Replays as POST /payment-intents (method=cash, captured locally) |
| 6 | Create reservation locally | WalkInConfirmation | INSERT INTO reservations_local (status confirmed_local) + outbox entries | Replays as POST /reservations with idempotency-key |
| 7 | Issue key (PIN / encoded card) | KeyIssuanceWizard | local PIN gen / encoder driver | Mobile-key (if applicable) issued on reconnect |
| 8 | Print folio receipt + voucher | desktop print shell | local PDF gen | — |
| 9 | (Reconnect) | SyncStatusPill: syncing | — | Outbox FIFO drain; events emitted to Pub/Sub; conflicts → Sync Center (W-12) |
Conflict policy on reconnect
- Inventory race (room sold by online channel during offline window): per
inventory-servicepolicy — server-authoritative; local reservation flagged withinventory_conflict; clerk notified via Sync Center (W-12) to resolve (re-assign or refund deposit). - Reservation aggregate is append-only on the local side; server reconciles by
clientMutationId. - Cash receipt is authoritative locally — never overridden by server. Cash drawer reconciliation (W-10) consumes the local truth.
Telemetry
frontend.desktop.walkin_started { offline: true } · frontend.desktop.walkin_confirmed_local { reservationLocalId } · frontend.desktop.outbox_drained { count, durationMs } · frontend.desktop.sync_conflict_surfaced { aggregate, count }.
W-08 — Housekeeping coordination + maintenance escalation
Actors. Housekeeping Lead · Housekeeper (kiosk sub-mode) · Maintenance Tech · Guest (mid-stay request).
Surfaces. Electron Desktop Backoffice (Lead view + Housekeeper kiosk mode); Consumer Mobile Manage tab (mid-stay request).
Primary BFF. bff-backoffice-service (operator) · bff-tenant-booking-service (guest mid-stay).
Services touched. housekeeping-service · maintenance-service · reservation-service (touchpoints) · ai-orchestrator-service (task ordering) · notification-service · staff-service (assignments).
Realizes. J-19, J-20, J-21, J-22 · EP-MEL-07 + EP-MEL-08.
Sub-workflows
| Sub | Trigger | Surface | Offline |
|---|---|---|---|
| Auto-task on checkout (J-19) | reservation.checked_out.v1 | Lead view: task auto-appears in queue with priority + AI-suggested order | Local outbox; tasks replay on reconnect |
| Offline housekeeping update (J-20) | Housekeeper kiosk picks up task | Kiosk mode (large touch targets, single-handed operation) | Full offline — all updates write to local SQLite; sync on reconnect |
| Mid-stay request (J-21) | Guest request via Manage tab | Mobile → tenant BFF → housekeeping queue | Online-only (guest writes) |
| Cleaning reveals maintenance issue (J-22) | Housekeeper escalation | Kiosk: "Raise maintenance ticket" → photo capture → ticket created | Local outbox; photos compressed and queued |
State (per task)
Step-by-step (housekeeper kiosk path, offline)
| # | Step | UI (kiosk mode) | Local action | Sync |
|---|---|---|---|---|
| 1 | Pick next task | HousekeeperKiosk (large tiles, AI-suggested order) | local read | — |
| 2 | Mark in-progress | tile flips state | local UPDATE | replays |
| 3 | Encounter maintenance issue | "Raise maintenance" CTA | photo capture (camera or upload) → local ticket draft | replays as POST /maintenance/tickets |
| 4 | Mark done | tile flips state | local UPDATE | replays |
| 5 | Lead spot-check | Lead view "Inspect" action | local UPDATE (status=inspected) | replays |
Role variants
| Persona | Variant |
|---|---|
| Housekeeping Lead | Full queue management, assignment, AI-order accept/dismiss, sign-off shifts |
| Housekeeper | Kiosk sub-mode — large tiles, single-task focus, escalation path; cannot reassign |
| Maintenance Tech | Maintenance queue (separate module) — triage, parts/labor logging, close-out |
| GM | Read-only across both queues; can override priority |
AI affordances
- Task ordering suggested per housekeeper based on room proximity, priority, ETA —
<AISuggestionCard />exposes "Why this order?" with the input features. Lead can accept (re-orders the queue) or dismiss (preserves manual order). - Photo quality scoring on maintenance ticket creation — flags blurry/dark photos and prompts retake before submission.
Offline fallback
Full offline. All housekeeping operations work offline; the kiosk is the most offline-tolerant surface in the platform. Mid-stay guest requests (consumer mobile) are online-only.
Telemetry
frontend.desktop.housekeeping_queue_opened · frontend.desktop.task_started { taskId } · frontend.desktop.task_completed { taskId, durationMs } · frontend.desktop.maintenance_escalated { taskId, ticketId } · frontend.desktop.ai_order_accepted / frontend.desktop.ai_order_dismissed.
W-09 — AI-assisted operations (pricing, dashboard, anomaly inbox)
Actors. GM · Front-Desk · Housekeeping Lead.
Surfaces. Electron Desktop Backoffice.
Primary BFF. bff-backoffice-service.
Services touched. ai-orchestrator-service (cloud + local inference adapters) · pricing-service · reservation-service · housekeeping-service · maintenance-service · notification-service · audit-service.
Realizes. J-15, J-16 · EP-MEL-11.
Surface composition
┌─ Daily Operations Dashboard (GM home) ────────────────────────────────┐
│ ┌───────────────────────┐ ┌────────────────────────────────────┐ │
│ │ Today KPIs │ │ AI Suggestions Inbox │ │
│ │ · ADR / RevPAR / occ │ │ · Pricing: +8% next weekend │ │
│ │ · arrivals / depart. │ │ · Housekeeping: deep-clean R204 │ │
│ │ · cash position │ │ · Maintenance: lock R301 battery │ │
│ │ · sync state │ │ · Anomaly: same card, 3 attempts │ │
│ └───────────────────────┘ └────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Activity Feed (live) │ │
│ │ · check-ins / check-outs / payments / key issuances / … │ │
│ └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Step-by-step (pricing suggestion accept, J-15)
| # | Step | UI | BFF | Provenance | HITL |
|---|---|---|---|---|---|
| 1 | Open AI inbox | AISuggestionsInbox | GET /ai/suggestions?inbox=open | — | — |
| 2 | Review suggestion card | <AISuggestionCard /> (pricing) — shows current rate vs proposed delta + horizon + confidence | — | footer: model · version · prompt · traceId · cloud | — |
| 3 | Open "Why this?" drawer | <AIExplanationDrawer /> | GET /ai/suggestions/{id}/explanation | — | — |
| 4 | Accept | dialog confirms scope (which rate plan, which date range) | POST /pricing/rate-plans/{id}/apply-suggestion (idempotency-keyed) | — | Counter-signature recorded with reviewedBy + reviewedAt |
| 5 | Audit + notification | toast with "View audit" link | audit-service writes; notification-service informs other GMs (multi-property) | — | — |
Anomaly inbox
- Same card used for ≥ 3 distinct payment attempts within 1 h →
<AIAnomalyCard />with payment trace; GM can flag the reservation, escalate to fraud review, or dismiss. - Cash drawer variance > tenant threshold → flagged; links to W-10 close.
- Lock vendor heartbeat missing for room R301 ≥ 30 min → maintenance pre-ticket created; surface in inbox.
Daily Operations Dashboard (J-16)
- Read-only KPI tiles (ADR, RevPAR, occupancy, cash position, arrivals/departures count, sync state).
- Activity feed (live event projection from
reservation.*,housekeeping.*,payment.*,lock.*events). - AI Insights inbox (unified feed of suggestions awaiting HITL).
Offline fallback
Partial. Dashboard KPIs derive from local SQLite (last-sync timestamp shown explicitly). AI inbox shows queued suggestions; new cloud-AI suggestions wait for reconnect; local-AI (edge ONNX) suggestions continue to fire (e.g., anomaly classifier on cash drawer).
Telemetry
frontend.desktop.ai_inbox_opened · frontend.desktop.ai_suggestion_viewed { id, kind } · frontend.desktop.ai_explanation_opened { id } · frontend.desktop.ai_suggestion_accepted { id, latency_ms } · frontend.desktop.ai_suggestion_dismissed { id, reasonCode }.
W-10 — End-of-day cash drawer close + regulatory submission
Actors. Front-Desk Clerk · Finance / Admin · Compliance Officer (signs regulatory exports).
Surfaces. Electron Desktop Backoffice (print + export shells).
Primary BFF. bff-backoffice-service.
Services touched. billing-service (folios, drawer balance) · payment-gateway-service (card settlement reconciliation) · reservation-service (guest registry projection) · tax-service (where applicable) · audit-service · file-storage-service (signed PDF storage).
Realizes. J-12, J-17, J-18 · EP-MEL-05 + EP-MEL-06.
Step-by-step (cash drawer close, J-12)
| # | Step | UI | BFF | Notes |
|---|---|---|---|---|
| 1 | Open cash drawer module | CashDrawerClose | GET /cash-drawer/today | — |
| 2 | Reconcile expected vs counted | DrawerReconcileForm (denomination breakdown) | POST /cash-drawer/{id}/reconcile { counted: { …denominations } } | Variance highlighted ≥ tenant threshold |
| 3 | Variance explanation (if any) | reason code + free text | (same call) | Mandatory if variance > 0 |
| 4 | Sign-off | clerk signature + supervisor signature (PIN step-up) | POST /cash-drawer/{id}/close | Both signatures recorded as audit fact |
| 5 | Print report | print shell | local PDF gen | A4 + Letter; bilingual header |
| 6 | Persist signed PDF | upload to file-storage-service | POST /file-storage/objects | Signed URL stored on close record |
Step-by-step (regulatory submission — daily guest registration, J-18)
| # | Step | UI | BFF | Notes |
|---|---|---|---|---|
| 1 | Open regulatory module | RegulatoryQueue | GET /regulatory/pending | Shows pending today + history |
| 2 | Select submission target | per-regulator schema (AF MoIA, TJ MIA, etc.) | GET /regulatory/schemas | — |
| 3 | Preview payload | RegulatoryPreview (PDF + XLSX + CSV per schema) | POST /regulatory/preview | — |
| 4 | Compliance officer signs | PIN step-up | POST /regulatory/submissions | Recorded as audit fact |
| 5 | Dispatch | regulator-specific channel (HTTPS API, SFTP, email) | notification-service outbound where applicable | Confirmation receipt persisted |
| 6 | Show receipt | toast + inbox entry | — | Receipt PDF stored in file-storage-service |
Step-by-step (monthly tax report, J-17)
Similar pattern: select period → preview → finance signs → export bundle (PDF + XLSX + CSV) → optional submission to tax authority adapter.
Offline fallback
- Cash drawer close can run offline against local SQLite. Reconciliation, variance, and sign-off all persist locally; PDF stored locally; outbox replays metadata to server on reconnect.
- Regulatory submission is online-only for actual dispatch; preview + sign-off can be prepared offline, but submission is held in outbox until connectivity returns.
- Tax export can be prepared offline; submission online-only.
Telemetry
frontend.desktop.cash_drawer_close_started · frontend.desktop.cash_drawer_variance_recorded { amount, reasonCode } · frontend.desktop.cash_drawer_closed { signedBy } · frontend.desktop.regulatory_submission_signed { regulatorId, submissionId } · frontend.desktop.regulatory_submission_dispatched { regulatorId, latency_ms }.
W-11 — Tenant onboarding + theming preview/publish/rollback
Actors. Hotel Owner · Platform Admin · Marketing Reviewer (HITL on AI-drafted content).
Surfaces. Control Plane (Phase 2) · Backoffice authoring console (today).
Primary BFF. Control Plane BFF (Phase 2) / bff-backoffice-service (interim).
Services touched. tenant-service · theme-config-service (drafts, preview, publish, rollback) · property-service (catalog) · iam-service (staff invites + roles) · notification-service (welcome email) · file-storage-service (logos, hero images).
Realizes. J-13, J-14 · EP-MEL-01.
State (per ThemeVersion)
Step-by-step (publish path)
| # | Step | UI | BFF | Validation gates |
|---|---|---|---|---|
| 1 | Open theme draft | ThemeAuthoringConsole | GET /theme-versions/{id} | — |
| 2 | Edit tokens / blocks / nav / flow | live preview iframe + side panel | PATCH /theme-versions/{id} (debounced) | inline contrast warnings via @ghasi/ui-melmastoon validators |
| 3 | Generate preview link | "Share preview" CTA | POST /theme-versions/{id}/preview | Returns PreviewToken (TTL ≤ 7 days; noindex/nofollow) |
| 4 | Stakeholder review (preview URL) | tenant booking SPA in preview mode (read-only) | GET /bootstrap?preview=… | POST /quote returns MELMASTOON.BFF.TENANT.PREVIEW_MODE_READ_ONLY |
| 5 | Publish | "Publish version" CTA | POST /theme-versions/{id}/publish | Token contrast, RTL parity, asset HEAD checks, AI-content HITL approval, booking-flow consistency |
| 6 | CDN bust + Memorystore rotation | toast "Live now — first request after publish ~30 ms latency" | (server-side workflow) | — |
| 7 | Audit | "View audit" link in toast | audit-service records publish | — |
Rollback (O(1))
| # | Step | UI | BFF |
|---|---|---|---|
| 1 | Open Versions tab | ThemeVersionsList | GET /themes/{id}/versions |
| 2 | Pick prior version | row-action "Restore this version" | POST /themes/{id}/rollback { versionId } |
| 3 | Confirm (typed verb ROLLBACK) | <ConfirmDestructiveDialog /> | (same call) |
| 4 | CDN bust + audit | toast "Rolled back" + "View audit" | — |
Failure modes
| Failure | UI behaviour |
|---|---|
| Contrast invariant violation | Inline error per failing token pair; publish CTA disabled; "Open contrast checker" link |
| RTL preset missing | Block publish; surface preset id and missing variant |
| AI-drafted content awaiting HITL | Block publish; surface review queue link |
| Asset HEAD failure (logo URL 404) | Inline error on the affected MediaRef; offer re-upload |
| OCC version mismatch (concurrent edit) | "This draft was updated elsewhere — refresh" |
Offline fallback
Online-only. Theme authoring requires authoritative server state, asset upload, and CDN bust.
Telemetry
frontend.theme.draft_opened { id } · frontend.theme.preview_issued { tokenId, ttlSeconds } · frontend.theme.publish_attempted { id } · frontend.theme.publish_failed { code } · frontend.theme.published { id, latency_ms } · frontend.theme.rolled_back { fromId, toId, reason }.
W-12 — Sync recovery & conflict resolution
Actors. Front-Desk Clerk · GM (override authority on conflicts) · Sync Engine (background).
Surfaces. Electron Desktop Backoffice — Sync Center module.
Primary BFF. bff-backoffice-service /sync/v1/pull + /sync/v1/push.
Services touched. every domain service (writes drain through outbox; conflicts surfaced).
Realizes. cross-cutting tail of W-07, W-08, W-10 (offline windows ending). · EP-MEL-10.
Sync Status Pill states (always visible top-right of desktop)
| State | Visual | Behaviour |
|---|---|---|
online | green dot + "Online" | All flows work normally; outbox empty |
syncing | rotating dot (or static, reduced-motion) + "Syncing N pending" | User can keep working; mutations append to outbox |
offline | amber dot + "Offline since HH:MM" | Online-only flows clearly disabled; offline-capable flows continue |
paused | grey dot + "Paused" | User-initiated pause (rare; debug + bandwidth-budget) |
conflicts | red dot + "N conflicts — resolve" | Click opens Sync Center directly to conflicts tab |
Sync Center anatomy
┌─ Sync Center ─────────────────────────────────────────────────────────┐
│ Tabs: [ Overview ] [ Outbox (12) ] [ Conflicts (2) ] [ Audit ] │
│ ┌─ Overview ──────────────────────────────────────────────────────┐ │
│ │ Last successful sync: 14:32 (28 min ago) │ │
│ │ Pending mutations: 12 │ │
│ │ Bandwidth used today: 2.1 MiB │ │
│ │ Next attempt: in 0:45 │ │
│ │ [ Force sync now ] [ Pause ] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Conflict resolution UI (per aggregate policy)
| Aggregate | Policy | UI |
|---|---|---|
Reservation | Append-only locally; server merges on clientMutationId | Side-by-side diff; rare conflicts (only inventory races) → see below |
Inventory | Server-authoritative | Conflict surfaces as "Room sold elsewhere" → reassign / refund deposit dialog |
Folio | LWW + diff (manual on conflict) | Side-by-side Keep Mine / Keep Server / Merge per line item |
KeyCredential | Append-only | No conflicts; vendor adapter resolves |
Theme draft | OCC | "Refresh and re-edit" |
HousekeepingTask | LWW (last writer wins) | Conflict only if Lead reassigns offline while another Lead reassigns online → dialog |
MaintenanceTicket | Append-only | No conflicts |
CashDrawerClose | Local truth (frontend authoritative) | Server reconciles to local; never overrides |
Step-by-step (conflict resolution, inventory race after walk-in)
| # | Step | UI | Action |
|---|---|---|---|
| 1 | Sync drains outbox; inventory race detected | toast "1 conflict — open Sync Center" + pill flips to red | — |
| 2 | Open Sync Center conflicts tab | SyncConflictList | — |
| 3 | Open conflict detail | SyncConflictDiff (side-by-side: local reservation vs server-confirmed online booking) | Shows guest names, dates, room |
| 4 | Pick resolution | ResolveConflictDialog — options: "Reassign to alternate room" | "Refund cash deposit" | "Escalate to GM" | — |
| 5 | Apply resolution | (per option) | local outbox → server replay; audit fact written |
| 6 | Notify guest | notification-service outbound (template per resolution) | — |
Offline / online behaviour
The Sync Center itself is always available. Force sync requires connectivity (button disabled when offline; tooltip explains why). Conflict diffs are always viewable from local SQLite; resolution actions write to local outbox first, then replay.
Telemetry
frontend.desktop.sync_status_changed { from, to } · frontend.desktop.outbox_drain_started { count } · frontend.desktop.outbox_drain_completed { count, durationMs, conflicts } · frontend.desktop.sync_conflict_opened { aggregate, id } · frontend.desktop.sync_conflict_resolved { aggregate, id, resolution }.
3. Notifications and outbound messaging
Every workflow emits user-visible notifications through notification-service. The frontend never composes outbound text directly; tenant-customizable templates flow through EmailTheme + LocalePack.
| Trigger | Default channel | Urgency | Template |
|---|---|---|---|
| Booking confirmed (cash) | SMS + WhatsApp + email | normal | booking.confirmed.cash |
| Booking confirmed (card) | SMS + email | normal | booking.confirmed.card |
| Pre-arrival reminder (T-24h) | SMS + push | normal | booking.reminder.24h |
| Mobile-key delivered | push + SMS | normal | key.delivered.mobile |
| Folio receipt | normal | folio.receipt | |
| Refund issued | email + SMS | normal | payment.refund |
| Mid-stay cleaning request acknowledged | push | normal | housekeeping.request.ack |
| Regulatory submission accepted | (operator-facing) inbox + email | normal | regulatory.accepted |
| Cash drawer variance > threshold | (operator-facing) inbox + push to GM | high | finance.drawer.variance |
| AI suggestion awaiting review | (operator-facing) inbox | low | ai.suggestion.queued |
| Sync conflict surfaced | (operator-facing) inbox + pill alert | high | sync.conflict.surfaced |
PII never leaks into push / SMS payloads — templates are generic; detailed content accessed only inside an authenticated surface.
4. Error and exception patterns (frontend surface)
| Class | Pattern |
|---|---|
| Validation errors | Inline, aria-describedby tie; surface stable error code in monospace badge for support escalation |
| ABAC denials | Neutral message + (where applicable) break-glass invitation with reason code |
| Licensing / tenant-suspended | Inline with "Contact platform admin" affordance; never raw 403 |
| Hold expiry | Non-blocking countdown banner at T-60 s; HoldExpiredBanner at T-0 with re-quote CTA |
| FX-snapshot stale | Non-blocking banner offers re-quote; previously held price honored until hold expires |
| Network loss | Sync pill (desktop) / connection banner (mobile + web) flips; in-flight mutations preserved; user informed non-blockingly |
| Card declined | Issuer code mapped to plain language; offer alternate method or cash-on-arrival; hold preserved |
| 3DS abandoned | Banner + retry from Payment screen |
| Vendor (lock) down | Wizard offers PIN fallback (W-05); banner explains the credential will be re-issued on reconnect |
| AI refusal | Clear, neutral; rephrase invitation; never presented as system error |
| Audit-write failure | The entire write is rolled back; user sees "Try again" |
| Sync conflict | Surfaced via pill + Sync Center (W-12); never auto-resolved on aggregates without LWW policy |
| Tenant suspended mid-session | Generic "Temporarily unavailable" page; no theme leak |
5. Cross-references
Every step in this document maps to a journey in docs/journeys/01-core-user-journeys.md and a story in docs/07-epics-and-user-stories.md. The mapping is intentional and bidirectional:
- Frontend changes that affect a workflow must update both this document and the journey file.
- Service changes that affect a workflow must update the service spec and (if the UI is materially different) this document.
- New workflows require a new
W-NNentry here, a correspondingJ-NN(or extension to an existing journey), and an epic linkage. Twenty hard-cap on workflows; new workflow requires ADR.
Per-workflow contract details (BFF endpoints, error codes, idempotency keys, FX-snapshot behaviour) live in the BFF API_CONTRACTS.md files referenced at the top of each workflow.
6. Why this set of workflows
These twelve workflows cover every persona, every surface, and every operational mode the platform supports. Each has an E2E gate because regressions on any of them would be noticed first by users in production:
- W-01 → W-04 are the revenue path — guest discovery to confirmed booking. Regression here is a direct loss of GMV.
- W-05 → W-07 are the operational path — the property's daily operations. Regression here means a clerk cannot check a guest in, which is a business-stopping incident in our Phase-1 markets.
- W-08 is the service-quality path — housekeeping and maintenance. Regression here cascades into guest dissatisfaction within hours.
- W-09 is the AI value path — pricing optimization, anomaly detection, dashboard intelligence. Regression here is a slow erosion of the AI-first commitment.
- W-10 is the financial integrity path — cash drawer close + regulatory submission. Regression here is a compliance and audit incident.
- W-11 is the tenant velocity path — onboarding and theming. Regression here means tenants cannot launch or adjust their brand.
- W-12 is the resilience path — sync recovery. Regression here breaks the offline-first promise and erodes operator trust in the platform.
Offline fallbacks follow desktop/06-desktop-app-specification.md §6 invariants. AI affordances follow 04-frontend-design-guidelines.md §10. Role-based UI variants reuse the same component tree — divergence is carried in Zustand role selectors, not in duplicate screens.
7. Open questions
- Whether the chain operator persona (Phase 2) gets an additional workflow (W-13: portfolio dashboard + cross-property rate strategy roll-out) or composes from W-09 + W-11 with a tenant switcher.
- Whether the TV-mode arrivals grid (kiosk display in lobby) gets a dedicated workflow or stays an UI-only variant of W-05.
- Final shape of the mid-stay in-app messaging between guest and property (W-08 sub-workflow vs new W-NN); pending Phase 2 scope decision in
01-web-and-mobile-specification.md§13.4. - Whether regulatory submission should split into per-regulator workflows (AF MoIA, TJ MIA, IR police) once the schemas diverge significantly, or remain a single W-10 sub-workflow with per-regulator preview templates.
- Whether the AI explainability drawer (W-09 step 3) gets a dedicated micro-workflow + telemetry surface, or stays inline.
8. References
- Frontend design guidelines (philosophy, principles):
04-frontend-design-guidelines.md - Web/mobile spec (stack, routing, state, offline, security):
01-web-and-mobile-specification.md - Theming & tenant config (token model, publish/rollback):
02-theming-and-tenant-config.md - Design system package (
@ghasi/ui-melmastoon):03-design-system.md - Desktop spec (Electron offline-first backoffice, SQLite, sync engine):
../desktop/21-desktop-app-specification.md - Core user journeys (J-01 … J-22, full operational atlas):
../journeys/README.md - Epics & user stories (EP-MEL-01 … EP-MEL-20):
docs/07-epics-and-user-stories.md - Event-driven architecture (subjects, sagas):
docs/04-event-driven-architecture.md - API design conventions:
docs/05-api-design.md - AI architecture (provenance, HITL, edge inference):
docs/08-ai-architecture.md - Lock & key integration:
docs/09-lock-and-key-integration.md - Payments architecture:
docs/10-payments-architecture.md - Security, compliance, multi-tenancy:
docs/07-security-compliance-tenancy.md - Testing strategy:
docs/11-testing-strategy-qa.md - Per-BFF API contracts:
bff-consumer-service·bff-tenant-booking-service·bff-backoffice-service