Skip to main content

14 — Core User Journeys

Companion: 01 Product Overview · 02 Enterprise Architecture · 04 Event-Driven Architecture · 05 API Design · 07 Security & Tenancy · 08 AI Architecture · 09 Lock & Key Integration · 10 Payments Architecture · 11 Frontend — Web & Mobile · 12 Frontend — Desktop · 13 Theming & Tenant Config

Service deep-dives referenced repeatedly in this document: reservation-service · housekeeping-service · lock-integration-service · billing-service · payment-gateway-service · notification-service · ai-orchestrator-service · search-aggregation-service · bff-consumer-service · bff-tenant-booking-service · bff-backoffice-service

This document is the canonical, end-to-end specification of the 22 core user journeys for Ghasi Melmastoon. Every journey is described in operational detail — not as marketing storytelling — so engineers, QA, and AI coding tools can implement, test, and audit each flow without re-deriving cross-service behaviour.

The desktop surface is Electron + Vite + React + SQLite (better-sqlite3) + ONNX Runtime Node. The cloud is GCP (Cloud Run, Cloud SQL, Pub/Sub, Memorystore, Cloud Storage, Secret Manager). These two facts are non-negotiable and appear repeatedly below; substitutions require an explicit unanimous ADR per README §Stack Commitments.


1. Introduction

1.1 Purpose

This is the operational atlas of the platform. Each journey captures:

  • A success path that an implementer can wire end-to-end against the per-service specs.
  • The branch decisions that change which sub-flow runs (e.g., card vs cash-on-arrival, online vs offline, single-property vs chain).
  • The error and failure paths that must be handled — vendor down, payment declined, lock issuance failing, sync conflict, AI provider rate-limited.
  • The offline paths specific to the Electron desktop (and the limited browse-cache offline on the consumer surfaces).
  • The events emitted at each material step so that downstream consumers (analytics, audit, sync, search, notification) can be reasoned about without re-reading per-service event schemas.
  • The telemetry every step produces, mapped to the KPIs in docs/01-product-overview.md §10.

1.2 How to read a journey

Every journey below uses the same structure:

SectionMeaning
GoalWhat the persona is trying to accomplish, in their words.
Persona / SurfacePrimary persona and the surface (Consumer Web, Consumer Mobile, Tenant Booking Web, Tenant Booking Mobile, Electron Desktop, Control Plane).
PreconditionsPlatform state, account state, configuration, and connectivity assumptions.
TriggerThe user action or system event that initiates the flow.
StepsA numbered list. Each step is a 4-tuple: UI surface (what the user sees) · System action (what services do) · Events emitted (Pub/Sub subjects from 04) · Data changed (which aggregate(s) and which fields).
Success criteriaConcrete, testable assertions that mark the journey complete.
Failure pathsBranches into error states; for each, the user-visible surface, the recovery action, and the saga compensation if any.
VariantsBranch points where the flow legitimately differs (single-room vs group, tenant locale, FX-snapshot present vs absent, lock vendor differences).
TelemetryFrontend frontend.* events + backend domain events that inform the KPIs in §10.

1.4 Cross-journey invariants

These apply to every journey below; they are not repeated per journey:

  1. Tenant on every request. Every API call carries X-Tenant-Id; every event carries tenantId; every SQLite row carries tenant_id. There are zero untenanted reads or mutations except the meta-search aggregate index.
  2. Idempotency on every mutation. All POST/PUT/PATCH carry Idempotency-Key. The desktop outbox carries a deterministic key per pending mutation. Replays are no-ops, not duplicates.
  3. Provenance on every AI step. Every AI call attaches aiProvenance = { model, version, promptId, traceId, reviewedBy?, reviewedAt?, local } per docs/08 §6.
  4. HITL on irreversible AI actions. Anything that mutates guest-facing state (rate change, message send, key issuance, refund) is suggested, never auto-applied, unless the tenant has enabled an explicit auto-policy for that surface.
  5. RTL/LTR by direction, not by locale. The shell honours dir="rtl" for Pashto/Dari/Persian/Arabic. Logical CSS only. Numerals follow tenant preference (Arabic-Indic vs European).
  6. Sync-status pill is always visible on the Electron desktop. Pending mutations and the connectivity state are reflected at every step in every journey.
  7. Audit log is canonical. Every state-changing step emits an entry to the immutable audit log via the audit-service subscription on the originating event.

2. Index of Journeys

#TitlePersonaPrimary surfaceComplexityCriticality
J-01Discover & Compare on Meta LayerGuestConsumer Web/MobileMP0
J-02Booking Handoff to Tenant SiteGuestConsumer → Tenant BookingSP0
J-03Multi-Step Booking with Cash on ArrivalGuestTenant Booking Web/MobileLP0
J-04Booking with Card Payment (3DS + FX snapshot)GuestTenant Booking Web/MobileLP0
J-05Arrive at Hotel & Check-InGuest + Front DeskElectron DesktopLP0
J-06Stay, Modify, Check-OutGuest + Front DeskElectron DesktopLP0
J-07Walk-In Booking with Cash Deposit (Offline)Front DeskElectron Desktop (offline)LP0
J-08Online Check-In Queue CoordinationFront DeskElectron DesktopMP1
J-09Mid-Stay Issue: Lock Battery DiesFront Desk + MaintenanceElectron DesktopMP1
J-10Group Booking with Multiple Rooms & Single FolioFront DeskElectron DesktopLP1
J-11Late Checkout & Folio DisputeFront Desk + ManagerElectron DesktopMP1
J-12End-of-Day Cash Drawer CloseFront Desk + FinanceElectron DesktopMP0
J-13Onboard New Tenant (Single Hotel Operator)OwnerControl Plane + ElectronLP0
J-14Onboard Chain (Multi-Property Tenant)Chain OperatorControl Plane + ElectronLP1
J-15Configure Dynamic Pricing AI SuggestionsGMElectron DesktopMP1
J-16Review Daily Operations DashboardGMElectron DesktopMP0
J-17Run Monthly Tax ReportFinanceElectron DesktopSP0
J-18Submit Daily Guest Registration to AuthorityReceptionist / AdminElectron DesktopMP0
J-19Auto-Cleaning on Checkout (Online)Housekeeping Lead + HousekeeperElectron DesktopMP0
J-20Offline Housekeeping Update from DesktopHousekeeping LeadElectron Desktop (offline)MP0
J-21Mid-Stay Cleaning RequestGuest + HousekeepingTenant Booking Mobile + DesktopSP1
J-22Cleaning Reveals Maintenance IssueHousekeeper + MaintenanceElectron DesktopMP0

3. Guest Journeys (J-01 → J-06)

J-01 — Discover & Compare on Meta Layer

Goal. A guest planning a weekend in Kabul wants to find a guesthouse with a halal kitchen, a prayer room, and parking, in the AFN 1,500 – 4,000 per-night band, then compare three candidates before deciding.

Persona / Surface. Guest · Consumer Web (Next.js App Router) and Consumer Mobile (React Native), both fronted by bff-consumer-service and search-aggregation-service.

Preconditions.

  • The PWA shell (web) or the React Native app (mobile) is installed/loaded with the latest design tokens from @ghasi/ui-melmastoon.
  • search-aggregation-service has indexed published properties (Phase 0 indexes the Afghan beachhead tenants) into Cloud SQL + a denormalized cache in Memorystore.
  • Locale is auto-detected (Pashto / Dari / Persian / EN); currency display defaults to the device locale with a manual switcher.

Trigger. Guest opens https://melmastoon.com (or launches the mobile app) and taps Find a stay.

Steps.

  1. Search inputUI: MetaSearchHero screen (location autosuggest, date pair, guests/rooms steppers, price band, amenities chip multi-select). · System: bff-consumer-service calls search-aggregation-service POST /api/v1/search/properties with { city, checkIn, checkOut, guests, rooms, priceBand, amenities, locale, currency }. · Events: melmastoon.search_aggregation.query.submitted.v1 (sampled). · Data: none persisted on the consumer surface; query is logged to the warm analytics sink.
  2. Results — list view (default)UI: MetaResultsList renders 20 result cards (provider name, hero photo, min/max nightly rate, availability badge, rating, distance from search anchor). Filters in left rail (web) / bottom sheet (mobile). · System: response includes signed image URLs from file-storage-service, FX-converted price ranges from pricing-service, and a paginated cursor. · Events: melmastoon.search_aggregation.result_set.served.v1. · Data: recent-search cache is updated locally (PWA IndexedDB / RN MMKV).
  3. Apply filter — Halal kitchen + Prayer roomUI: user toggles two amenity chips. · System: bff-consumer-service re-issues the search; cached partial results are reused if filter is additive (≤ 250ms re-render). · Events: melmastoon.search_aggregation.filter_applied.v1. · Data: filter set persisted to the device for the session.
  4. Switch to map viewUI: MetaResultsMap (Leaflet on web; react-native-maps on mobile). Pins cluster at zoom-out; tapping a pin opens a quick-info popover with photo + min price + rating. · System: same query reused; bounding-box re-query when the user pans more than the threshold. · Events: melmastoon.search_aggregation.viewport_changed.v1 (debounced).
  5. Compare three propertiesUI: user taps the Compare affordance on three cards; opens MetaCompareDrawer with a side-by-side table (price, amenities, rating, cancellation policy, distance, halal certified flag). · System: GET /api/v1/search/compare?ids=…; response is a denormalized projection. · Data: compare set in session.
  6. Open property detailUI: user taps the second result; PropertyDetailScreen renders gallery (lazy-loaded thumbnails → full), amenities, policies, rooms preview, reviews, map. · System: bff-consumer-service GET /api/v1/properties/:id aggregates property-service (catalog) + pricing-service (rate band) + notification-service rating projection (post-stay surveys). · Events: melmastoon.search_aggregation.property.viewed.v1.
  7. Decide to proceedUI: tap Book on hotel site → transitions to J-02.

Success criteria.

  • LCP ≤ 2.5s on a 3G profile for the results screen; compare drawer opens in ≤ 300ms.
  • All copy renders in user's locale (Pashto/Dari/Persian/EN); RTL layout is correct end-to-end.
  • Min/max prices reconcile with pricing-service snapshot within ±0.5%.

Failure paths.

  • Search timeout (search-aggregation-service p99 breached). Banner: "Slow connection — showing recent results." Cached PWA result set is rendered if available.
  • No results in region (Phase 0 has limited geographic coverage). Empty state: "We're not in this city yet — tell us where to launch next" + waitlist email capture.
  • Currency unavailable (FX feed stale > 24h for the target currency). Display falls back to USD with a tooltip explaining the snapshot freshness.
  • Image load failure. Skeleton placeholder; provider name + rate still visible; no broken-image icon.

Variants.

  • Mobile vs Web map. Web uses Leaflet with raster tiles; mobile uses react-native-maps over OSM tiles. Both share the pin component contract.
  • Locale-driven calendar. Pashto/Dari/Persian users see Solar Hijri presentational dates with Gregorian on hover/tap.
  • Anonymous vs returning guest. Returning guests get a "Resume search" pill from session storage.

Offline path. PWA service worker serves the last successful results page from IndexedDB; mobile RN reads from MMKV. The "Offline — cached results" pill is shown. New searches are queued client-side and re-run on reconnect.

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 } · KPI: meta-layer conversion (eventual booking attribution to this traceId).


J-02 — Booking Handoff to Tenant Site

Goal. Transition the guest from the meta layer into the tenant-themed booking experience without losing context (dates, guests, rooms, locale).

Persona / Surface. Guest · Consumer surface → Tenant Booking surface (bff-tenant-booking-service, theme-config-service).

Preconditions.

  • The selected tenant is active (not suspended) and has a published theme version and at least one published rate plan.
  • Both BFFs share the platform's request-context middleware (tenant resolution, locale, traceparent propagation).

Trigger. Guest taps Book on hotel site from J-01 step 7.

Steps.

  1. Generate signed handoff URLUI: loading transition. · System: bff-consumer-service calls POST /api/v1/handoff with { tenantId, propertyId, search: { checkIn, checkOut, guests, rooms }, locale, currency, traceId }; response is { url, expiresAt }. The URL is signed (HMAC over the payload, 5-minute TTL). · Events: melmastoon.bff_consumer.handoff.issued.v1.
  2. Tenant subdomain bootstrapUI: navigation to https://<tenant-slug>.melmastoon.com/book?token=… (or deep link melmastoon://tenant/<slug>/book?token=…). · System: bff-tenant-booking-service validates the signed token, resolves tenantId, fetches the published theme via GET /api/v1/themes/published?tenantId=…, fetches the booking-flow config (steps order, optional fields, payment options enabled), and hydrates a tenant-context object server-side (Next.js RSC / RN context provider). · Events: melmastoon.bff_tenant_booking.session.started.v1.
  3. Render tenant themeUI: logo, color tokens, type ramp, layout preset, content blocks resolve. Dates and guests pre-populated from the handoff payload. · System: design tokens applied at runtime; no per-tenant fork.
  4. Continue into J-03 / J-04.

Success criteria.

  • Handoff completes in ≤ 1.5s p95 on a 3G profile.
  • Pre-populated dates/guests are identical to the meta-layer search.
  • The signed token is single-use and bound to the originating IP class (best-effort; not a hard block, but logged).

Failure paths.

  • Signed link expired (TTL > 5 min). Page shows "This booking session expired — start again" with a back link to the meta property page; a fresh handoff is generated transparently if the user clicks back.
  • Unknown tenant slug (suspended, deleted, or renamed). Generic "This hotel isn't accepting direct bookings right now" page; CTA to return to meta.
  • Theme version unpublished (a recent revert is mid-deploy). Falls back to the last known good version; surfaces an internal-only alert to the tenant's GM via the desktop notification feed.

Variants.

  • Direct landing without handoff (guest types https://<slug>.melmastoon.com directly). The same bff-tenant-booking-service bootstrap runs, minus the dates/guests pre-fill; user picks them in J-03 step 1.
  • Mobile deep link uses the same signed token but opens via Universal Link / App Link, falling back to the web URL if the app is not installed.

Telemetry. frontend.tenant.booking_session_started { tenantId, source } · frontend.tenant.theme_loaded { themeVersion, latency_ms } · frontend.tenant.handoff_invalid { reason }.


J-03 — Multi-Step Booking with Cash on Arrival

Goal. A guest books two rooms (1 family, 1 standard upgraded to deluxe) for two nights, captures both Pashto and Latin-script guest names, selects Cash on Arrival, and receives a multilingual confirmation.

Persona / Surface. Guest · Tenant Booking Web/Mobile.

Preconditions.

  • Tenant has enabled cash_on_arrival on the relevant property and rate plan (payment-gateway-service policy policy.cash_on_arrival_enabled = true).
  • Inventory is available for the requested window.
  • Locale resolves to Pashto with Dari fallback for free-text.

Trigger. Guest is in the tenant booking flow (handed off from J-02, or landed directly).

Steps.

  1. Select room typesUI: RoomSelectionScreen shows room cards with photos, occupancy, base rate, and amenities. Stepper to add. · System: bff-tenant-booking-service POST /api/v1/quotes → forwards to reservation-service RequestQuoteUseCasepricing-service getQuote. · Events: melmastoon.reservation.quote.created.v1 (TTL = 10 min). · Data: Quote projection persisted (no aggregate yet).
  2. Upgrade standard → deluxeUI: in-line upgrade card (priced delta shown). · System: re-quote on the same Idempotency-Key; returns { quoteId, items[], fxSnapshot, total }. · Events: none net-new (re-quote does not emit a separate event; it overwrites the projection).
  3. Enter guest detailsUI: GuestDetailsForm collects primary guest (name, email, phone, document) and additional guest (name only). The name fields render two paired inputs: native script (Pashto) + Latin transliteration (auto-suggested via ai-orchestrator-service transliterateName, HITL-edited by the guest). · System: AI suggestion call returns { given, family, confidence } with provenance; user can accept or override. · Events: melmastoon.ai_orchestrator.suggestion.served.v1.
  4. Select payment method = Cash on ArrivalUI: PaymentMethodScreen lists allowed methods (Cash on Arrival, Card if enabled, PayPal if enabled). User picks Cash on Arrival; consent text in tenant locale: "You will pay AFN at check-in." · System: payment-gateway-service POST /api/v1/intents with method=cash_on_arrival; returns intentId in pending_cash state. · Events: melmastoon.payment.intent.created.v1.
  5. Confirm bookingUI: ReviewScreen summarises everything; Confirm CTA. · System: reservation-service HoldReservationUseCase runs the saga SAGA-RESERVATION-CONFIRM-CASH: hold inventory (inventory-service.hold), then immediately confirm (no card auth needed) (ConfirmReservationUseCase), lock the FX snapshot, persist Reservation in confirmed state, append melmastoon.reservation.confirmed.v1. Lock issuance is deferred (stay starts in the future). Notification fires. · Events emitted: melmastoon.reservation.held.v1melmastoon.inventory.allocation.committed.v1melmastoon.reservation.confirmed.v1melmastoon.notification.message.queued.v1 (× channels). · Data: Reservation aggregate persisted; folio opened with pending_cash charge; key credential row pre-allocated in lock-integration-service with status = scheduled.
  6. Confirmation pageUI: BookingConfirmationScreen shows reservation code, dates, rooms, guests (in both scripts), total in AFN with USD equivalent (FX-snapshot), payment instructions ("Pay AFN in cash at check-in"), Add-to-Calendar buttons, share link, and a multilingual confirmation pre-rendered (Pashto primary + EN secondary). · System: notification-service dispatches confirmation via SMS (primary in low-connectivity markets), WhatsApp if a number is detected as WA-active, and email.
  7. Pre-arrival message (T-24h, async)notification-service schedules a 24h-before reminder in the guest's locale.

Success criteria.

  • The booking is confirmed end-to-end in ≤ 4s p95 on a 3G profile.
  • The FX snapshot at confirm exactly matches what is shown on the confirmation page (no drift).
  • SMS reaches the guest in ≤ 30s p95; WhatsApp in ≤ 10s p95 if active; email in ≤ 60s p95.
  • The reservation is searchable in the tenant's Electron desktop arrivals grid within 5s (event projection lag).

Failure paths.

  • Room becomes unavailable mid-flow (between quote and confirm). The hold step returns MELMASTOON.INVENTORY.NO_AVAILABILITY; UI surfaces an alternate-room offer (same room type, equivalent room) or invites the guest to change dates. No charge incurred.
  • Guest abandons after hold (closes browser). Hold expires after 10 minutes; inventory-service releases allocation; reservation-service transitions hold → expired; melmastoon.reservation.hold.expired.v1 is emitted; analytics records funnel drop.
  • Notification channel down (e.g., SMS provider outage). Notification queues into notification-service outbox with backoff; the confirmation page still renders successfully (independent flow).
  • Tenant disables cash-on-arrival mid-session. The payment step shows a "This option is no longer available" message; the user is offered card or PayPal where enabled; the in-flight quote remains valid for its TTL.
  • AI transliteration unavailable. The Latin-script field falls back to a manual input with placeholder text and validation rules; no booking blockage.

Variants.

  • Single room vs multi-room vs group (J-10).
  • Sharia-compliant tenant disables card payments and surfaces only cash-on-arrival + Hawala-mediated bank transfer.
  • Locale variations — Persian/Dari/Pashto all render RTL with locale-specific numeral preferences.

ASCII sequence diagram.

Guest Tenant Booking UI bff-tenant-booking reservation-service inventory-service pricing-service payment-gateway-service lock-integration-service notification-service Pub/Sub
│ │ │ │ │ │ │ │ │ │
│ select rooms │ │ │ │ │ │ │ │ │
├────────────────►│ │ │ │ │ │ │ │ │
│ │ POST /quotes │ │ │ │ │ │ │ │
│ ├────────────────────────►│ RequestQuote │ │ │ │ │ │ │
│ │ ├──────────────────────►│ getQuote │ │ │ │ │ │
│ │ │ ├──────────────────────►│ │ │ │ │ │
│ │ │ │ ├─────────────────────►│ │ │ │ │
│ │ │ │ ◄─── priced quote ────┴──────────────────────┘ │ │ │ │
│ │ │ ◄── quote(id, items, fx) ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────►│ quote.created.v1 │
│ │ │ │ │ │ │ │ │ │
│ pick C-O-A │ │ │ │ │ │ │ │ │
├────────────────►│ POST /payment-intents │ │ │ │ │ │ │ │
│ ├────────────────────────►│ │ │ ├─────────────────────────►│ createIntent(cash) │ │ │
│ │ │ ◄────────────────── intent(pending_cash) ──── │ │ │ │ │ │
│ confirm │ │ │ │ │ │ │ │ │
├────────────────►│ POST /reservations │ │ │ │ │ │ │ │
│ ├────────────────────────►│ HoldReservation │ │ │ │ │ │ │
│ │ ├──────────────────────►│ hold(allocs) │ │ │ │ │ │
│ │ │ ├──────────────────────►│ │ │ │ │ │
│ │ │ │ ◄── allocations ──────┤ │ │ │ │ inventory.allocation.committed.v1 │
│ │ │ │ ConfirmReservation │ │ │ │ │ │
│ │ │ ├──── persist + outbox │ │ │ │ │ │
│ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────►│ reservation.confirmed.v1 │
│ │ │ │ │ │ │ schedule key (deferred) │ │ │
│ │ │ │ │ │ ├─────────────────────────────►│ status=scheduled │ │
│ │ │ │ │ │ │ │ │ sendConfirmation │
│ │ │ │ │ │ │ │ ─────────────────────────────►│ │
│ ◄── confirmation page ────────────────── │ │ │ │ │ │ │ │
│ ◄── SMS/WA/Email ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │

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 } · melmastoon.reservation.confirmed.v1 · KPI: direct-booking share, meta→booking conversion, time-to-confirm p95.


J-04 — Booking with Card Payment (3DS + FX snapshot)

Goal. A guest books a single deluxe room for three nights and pays by Visa/Debit, completing a 3DS challenge; the tenant settles in AFN, the guest sees both AFN and the card's billing currency (USD) with FX-snapshot transparency.

Persona / Surface. Guest · Tenant Booking Web/Mobile.

Preconditions.

  • Tenant has card payments enabled and a Stripe (or equivalent) account connected via payment-gateway-service.
  • The card-issuer region triggers 3DS (regulatory or risk-rule).

Trigger. Guest reaches the payment step in the tenant booking flow.

Steps.

  1. Quote + hold identical to J-03 steps 1–3.
  2. Select payment method = CardUI: PaymentMethodScreen shows Stripe Elements card form (number, expiry, CVC, cardholder name, billing country). The total is displayed twice: AFN primary, USD secondary, with an info tooltip "FX-snapshot from at ". · System: payment-gateway-service creates a PaymentIntent via the Stripe adapter; the intent carries requires_action for 3DS. · Events: melmastoon.payment.intent.created.v1.
  3. 3DS challengeUI: the Elements iframe opens the card-issuer 3DS modal; the user authenticates (OTP, biometric on mobile, etc.). · System: the SDK reports paymentIntent.status = succeeded to the client; payment-gateway-service simultaneously receives the Stripe webhook (payment_intent.succeeded) and emits melmastoon.payment.intent.captured.v1.
  4. Reservation confirmreservation-service consumes payment.intent.captured.v1, runs ConfirmReservationUseCase, locks the FX snapshot (the snapshot used for the price the user saw is the snapshot persisted to the reservation), persists Reservation in confirmed, opens the folio with the captured charge, schedules deferred key issuance.
  5. Confirmation identical to J-03 step 6, with paymentMethod = card and the captured card brand/last4 displayed for the receipt.

Success criteria.

  • The user sees a single "Confirmed" page at most 5s after pressing "Pay" (excluding 3DS user time).
  • The folio shows the captured payment immediately, not "pending".
  • The FX snapshot used at quote = the FX snapshot stored on the reservation = the FX snapshot on the receipt.

Failure paths.

  • Card declined. UI re-prompts with the issuer's failure code mapped to plain language ("Your card was declined. Try another card or pay cash on arrival."). The hold remains valid until its TTL; user can retry.
  • 3DS timeout (user navigates away or the issuer times out). The intent transitions to requires_payment_method; user can re-attempt or change method.
  • Fraud anomaly (ai-orchestrator-service detectAnomalies returns verdict = block). The intent is held in requires_review; the guest sees "Manual review required — we will email you within 4 hours"; a HITL queue entry is created in the tenant's desktop alert feed (J-16).
  • Webhook delay (Stripe webhook lag > 30s). The client polls GET /api/v1/payment-intents/:id for status; UI shows a spinner with a "Verifying payment…" affordance and an explicit "Don't refresh" hint.
  • Currency feed stale. The flow prevents confirm if the FX feed staleness exceeds the tenant-configured threshold (default 6h); user is shown a clear message and asked to retry shortly.

Variants.

  • PayPal uses the same flow with the PayPal SDK; no 3DS step but a PayPal-side approval.
  • MFS adapter (e.g., M-Pesa, JazzCash in Phase 1) replaces 3DS with the operator's USSD/STK-push flow.

Telemetry. frontend.booking.method_picked { method: "card" } · frontend.booking.three_ds_started · frontend.booking.three_ds_completed { duration_ms } · frontend.booking.payment_failed { reason } · melmastoon.payment.intent.captured.v1 · melmastoon.reservation.confirmed.v1.


J-05 — Arrive at Hotel & Check-In

Goal. A confirmed guest arrives at the front desk; the clerk identifies them by reservation code or last name, scans the guest's photo ID, prints/encodes the key (RFID card or sends a mobile key), and seats the guest.

Persona / Surface. Guest + Front Desk · Electron Desktop (bff-backoffice-service + reservation-service + lock-integration-service + ai-orchestrator-service).

Preconditions.

  • Reservation is in state confirmed; the room is in state clean (read from housekeeping-service projection); the property has a paired lock device or on-prem encoder.
  • Front-desk user is authenticated, device is bound, and they have role front_desk with permission reservation.checkin.

Trigger. Guest presents at front desk with a reservation code (or just a name).

Steps.

  1. Lookup reservationUI: ArrivalsBoard (default screen at the front desk). Search field accepts code, last name (in any script), or phone last 4. · System: local SQLite read first; if connected, refresh from bff-backoffice-service. · Data: read-only.
  2. Open Check-In drawerUI: CheckInDrawer shows reservation summary, room number, special requests, ID requirements, payment status. CTA Start check-in. · System: reservation-service StartCheckInUseCase is invoked → state transitions confirmed → check_in_started. · Events: melmastoon.reservation.check_in_started.v1. · Data: aggregate version bumps; pendingSagaStep = await_key_issued.
  3. Capture IDUI: IdCaptureCard activates the webcam (or scanner); guest places passport/ID; clerk taps Scan. · System: image is sent to ai-orchestrator-service ocrIdentityDocument (edge ONNX model first; cloud Vertex Document AI if confidence < 0.7 and online). Returns { documentNumber, name, dateOfBirth, nationality, expiry, confidence, fields[] }. UI surfaces the parsed fields with each highlighted; clerk HITL-confirms before saving (per 01 §1.3). · Events: melmastoon.ai_orchestrator.suggestion.served.v1. · Data: parsed fields persisted on the Reservation.guests[] aggregate (PII; encrypted column).
  4. Sign registration cardUI: SignaturePad (Phase 2: tablet) or Print + collect (paper today). · System: signed PDF or a flag on the aggregate marking "signed (paper)"; PDF stored in file-storage-service with retention per tenant policy.
  5. Issue keyUI: KeyIssuanceCard shows three CTAs based on tenant policy: Encode RFID, Send mobile key, Issue both. Clerk picks per the guest's preference. · System:
    • Encode RFID: desktop talks to the on-prem encoder via the local USB/serial adapter exposed as a lock-integration-service local agent. Returns { credentialId, encodedAt }.
    • Mobile key: lock-integration-service IssueKeyCredentialUseCase → vendor adapter (TTLock / Salto / etc.); returns a tokenized credential; notification-service sends an SMS/WhatsApp with the deep link or PIN. · Events: melmastoon.lock_integration.key_credential.issued.v1.
  6. Complete check-inUI: clerk taps Complete check-in. · System: reservation-service CompleteCheckInUseCase consumes the key_credential.issued.v1 event (or accepts a manual override requiresManualKey=true); state transitions check_in_started → checked_in; emits melmastoon.reservation.checked_in.v1; billing-service opens the operational folio if not already open. · Data: room status flips to occupied via housekeeping-service listener.
  7. Welcome messageUI: a friendly toast in the guest's locale; print an arrival card if tenant policy is enabled.

Success criteria.

  • End-to-end check-in time ≤ 3 minutes p50 / ≤ 5 minutes p95 (front-desk start → key issued).
  • ID OCR confidence ≥ 0.9 on accepted documents; HITL confirmation always required regardless of confidence.
  • Key issuance success rate ≥ 99% across vendor adapters under healthy conditions.

Failure paths.

  • ID OCR fails (low confidence, glare, partial scan). Clerk falls back to manual entry; the AI provenance log records the override; no blockage.
  • Guest arrived early. Policy check via pricing-service rate plan; if earlyCheckInAllowed=true or front-desk override is permitted, proceed with policyOverride audit; otherwise offer luggage hold and a friendly ETA.
  • Room not yet inspected (housekeeping-service says clean=false). UI surfaces a banner with the housekeeping task ID and a "Notify housekeeping" CTA which bumps task priority via BumpPriorityUseCase (J-19).
  • Lock vendor down. UI shows the vendor health badge; clerk falls back to a pre-encoded backup card pool (cache of 5 spare cards per property); credential is reconciled at vendor recovery.
  • Mobile key SMS fails. Notification queues with backoff; UI shows pending state; clerk can manually print a fallback PIN slip from the lock-integration-service issued credential payload.

Variants.

  • Walk-in (J-07) bypasses lookup and creates the reservation in the same flow.
  • Group arrival (J-10) batches step 2 across N reservations sharing a folio.
  • VIP guest (flag set on Reservation.guestProfile.tier = "vip") routes to a manager affordance for a personal greeting.

ASCII sequence diagram.

Front Desk Clerk Electron Desktop bff-backoffice-service reservation-service ai-orchestrator-service lock-integration-service notification-service Pub/Sub
│ │ │ │ │ │ │ │
search guest │ │ │ │ │ │ │
├──────────────────► │ local SQLite hit │ │ │ │ │ │
│ │ + (online) GET /reservations?q=... │ │ │ │ │
│ ├──────────────────────────►│ ───────────────────────► │ │ │ │ │
│ ◄───── reservation card ───────────────────────────────────────────────── │ │ │ │
│ Start check-in │ │ │ │ │ │ │
├──────────────────► │ POST /reservations/:id/check-in/start │ │ │ │ │
│ ├──────────────────────────►│ StartCheckInUseCase ────►│ persist + outbox │ │ │ check_in_started.v1 │
│ scan ID │ │ │ │ │ │ │
├──────────────────► │ camera capture │ │ │ │ │ │
│ │ POST /ai/ocr-document │ │ │ │ │ │
│ ├──────────────────────────►│ ──────────────────────────────────────────────────────►│ run OCR (edge → cloud) │ │ │
│ ◄── parsed fields │ ◄──── HITL: confirm ────────────────────────────────────────────────────────────── │ │ │ │
│ Issue key (RFID) │ │ │ │ │ │ │
├──────────────────► │ local USB encoder OR vendor SDK call │ │ │ │ │
│ ├──────────────────────────►│ POST /key-credentials ──┴──────────────────────────────────────────────────────────►│ IssueKeyCredentialUseCase │ │
│ │ │ │ → vendor adapter │ │
│ │ ◄────────── credentialId ─────────────────────────────────────────────────────────────────────────────────────── │ │ key_credential.issued.v1 │
│ Complete check-in │ │ │ │ │ │ │
├──────────────────► │ POST /reservations/:id/check-in/complete │ │ │ │ │
│ ├──────────────────────────►│ CompleteCheckInUseCase ─►│ persist + outbox │ │ │ checked_in.v1 │
│ │ │ │ │ │ │ sendArrivalGreeting │
│ ◄── confirmation, key handed over ──────────── │

Telemetry. frontend.checkin.start_clicked · frontend.checkin.id_scanned { confidence } · frontend.checkin.id_hitl_overridden · frontend.checkin.key_issued { vendor, kind, latency_ms } · frontend.checkin.complete_clicked · melmastoon.reservation.checked_in.v1 · KPI: time-to-check-in p95, key-issuance success rate per vendor.


J-06 — Stay, Modify, Check-Out

Goal. A guest extends their stay by one night, charges mini-bar consumption, pays the remaining balance by card at checkout, has the key revoked, and receives a thank-you message in their language.

Persona / Surface. Guest + Front Desk · Electron Desktop.

Preconditions.

  • Reservation in state checked_in; folio open; key credential active.
  • Front-desk user has role front_desk with permissions reservation.modify, folio.charge, payment.collect, reservation.checkout.

Trigger. Guest approaches front desk to request a one-night extension and pay.

Steps.

  1. Modify reservation — date_changeUI: ReservationDetailModify → date stepper. · System: reservation-service ModifyReservationUseCase (sub-type date_change) → re-quote → reallocate inventory → check next reservation conflict → if no conflict, charge differential to folio → update key credential validity (UpdateKeyCredentialUseCase). · Events: melmastoon.reservation.dates_changed.v1, melmastoon.lock_integration.key_credential.updated.v1. · Data: Reservation.stayWindow extended; folio gains a charge line.
  2. Add mini-bar charges to folioUI: FolioDrawerAdd charge → select preset (mini-bar items) or custom amount. · System: billing-service PostChargeUseCase. · Events: melmastoon.billing.charge.posted.v1. · Data: immutable folio entry.
  3. Initiate checkoutUI: CheckOutDrawer shows folio totals (charges, taxes, payments, balance owed). · System: reservation-service StartCheckOutUseCase → state checked_in → check_out_started. · Events: melmastoon.reservation.check_out_started.v1.
  4. Collect remaining payment by cardUI: PaymentCollectionCard opens; clerk inserts/taps card on the on-prem terminal (or guest pays via QR for tap-to-pay). · System: payment-gateway-service creates and captures intent; on success, billing-service posts the payment to the folio. · Events: melmastoon.payment.intent.captured.v1, melmastoon.billing.payment.posted.v1.
  5. Complete checkoutUI: clerk taps Complete checkout. · System: reservation-service CompleteCheckOutUseCase; state check_out_started → checked_out; key revoked (RevokeKeyCredentialUseCase); housekeeping task auto-created (J-19); folio sealed (no more posts). · Events: melmastoon.reservation.checked_out.v1, melmastoon.lock_integration.key_credential.revoked.v1, melmastoon.housekeeping.task.created.v1. · Data: room state occupied → dirty.
  6. Thank-you messageUI: a small affordance to send a thank-you (auto by default if tenant policy enabled). · System: notification-service sends per the guest's preferred channel/locale.

Success criteria.

  • Checkout completes in ≤ 2 minutes p50.
  • Folio invoice (PDF) is generated within 10s of checkout and emailed/SMS-linked.
  • Key revocation is acknowledged by the lock vendor within 30s p95.

Failure paths.

  • Extension blocked by next reservation conflict. inventory-service.reallocate returns NO_AVAILABILITY; UI surfaces an alternate-room offer with a clear cost diff and key re-encoding warning; if the guest accepts, the room-change sub-saga runs (revoke old key → encode new).
  • Folio dispute (J-11). The clerk applies a credit note with manager approval; the corrected invoice is re-issued.
  • Refund flow for early checkout. A ProcessRefundUseCase runs against payment-gateway-service; the refund is asynchronous and surfaces in the folio as pending_refund until the provider confirms.
  • Card terminal offline. Fallback to cash collection or a pay-by-link sent to the guest's phone; reservation can complete in checked_out_with_balance state with a settlement deadline; an alert lands on the GM's dashboard.

Variants.

  • Auto-checkout when guest pays online and skips the front desk (Phase 2): same flow runs from a guest action in the tenant booking app.
  • Disputed final charge routes through J-11.
  • Late checkout is a special case of extension (sub-day) with a different rate-plan rule.

Telemetry. frontend.reservation.modify_date_change · frontend.folio.charge_posted { source } · frontend.checkout.start · frontend.payment.collected { method } · frontend.checkout.complete · melmastoon.reservation.checked_out.v1 · KPI: avg checkout time, EOD variance.


4. Front-Desk Journeys (J-07 → J-12)

J-07 — Walk-In Booking with Cash Deposit (Offline)

Goal. At 23:00 with no internet, a walk-in guest needs a room for one night. The clerk creates the reservation, takes a cash deposit, encodes an RFID card via the on-prem encoder, and hands over the key — all from the offline Electron desktop.

Persona / Surface. Front Desk · Electron Desktop (offline).

Preconditions.

  • Desktop has a valid offline session (token cached; device bound; offline window within policy, default ≤ 24h).
  • Local SQLite has a recent snapshot (reservations ± 30 days, room status, rate plans, lock-vendor offline-issuance certificate).
  • The on-prem encoder supports offline encoding; lock vendor's offline issuance certificate is valid (issued by lock-integration-service MintOfflineIssuanceCertificateUseCase).

Trigger. Walk-in guest at the desk asking for a room.

Steps.

  1. Find available roomUI: RoomGrid (offline view, sourced from SQLite). Filters by room type, occupancy, accessible. · System: local read; no network.
  2. Create walk-in reservationUI: WalkInDrawer collects guest name (Pashto + Latin via local Unicode keyboard; AI transliteration is unavailable offline so manual entry only), phone, document, expected nights, room. · System: bff-backoffice-service (local sidecar) writes a provisional reservation to SQLite with status = held_offline, generates a temporary code (OFF-XXXXX), allocates the room locally with a per-aggregate conflict policy (reservation-service server-side will arbitrate at sync). · Events: none yet (will be emitted at sync as melmastoon.reservation.held.v1 + melmastoon.reservation.confirmed.v1).
  3. Take cash depositUI: PaymentDrawer opens with method = Cash; clerk enters the amount taken. · System: billing-service (local mirror) writes a pending_payment row to the folio with status = pending_sync. · Events: outbox-queued melmastoon.billing.payment.posted.v1.
  4. Encode RFID card via on-prem encoderUI: KeyIssuanceCard calls the local encoder agent. · System: the lock-integration-service local agent uses the offline-issuance certificate to mint a credential the door lock will accept; writes a provisional credential to SQLite with vendorRef = pending_sync. · Events: outbox melmastoon.lock_integration.key_credential.issued.v1 (provisional flag).
  5. Print receiptUI: prints to the local thermal printer; receipt includes a "Provisional — pending sync" footer.
  6. Hand over key, seat guest.
  7. At reconnect (next morning at 06:00)System:
    • Sync engine pulls server-state delta first; reconciles by tenant + property; checks for any conflicting reservations created elsewhere (e.g., a same-room booking that came in via meta layer overnight).
    • If no conflict: push the outbox in order. reservation-service server runs ConfirmReservationUseCase accepting the offline-allocation evidence; inventory-service commits; billing-service posts payment; lock-integration-service ReconcileProvisionalCredentialsUseCase reconciles the credential.
    • If conflict (rare, given the per-aggregate locking and the human-driven walk-in): the desktop surfaces a conflict in the Sync Center with a one-click "Reaccommodate to Room " affordance; the lock credential is revoked and re-issued accordingly.

Success criteria.

  • The entire offline walk-in flow completes in ≤ 4 minutes; no operator-facing error states under normal hardware health.
  • At reconnect, the reservation reaches confirmed server-state without operator intervention in ≥ 95% of cases (target).
  • Cash deposit reaches the EOD reconciliation for the correct shift; no orphan rows.

Failure paths.

  • Encoder unavailable. Clerk hands a paper key or master-card; reservation flag requiresManualKey = true; reconciliation issues a vendor credential at sync if the room supports remote issuance.
  • Sync conflict (e.g., overlapping online reservation for same room). Sync Center prompts the GM/clerk; alternate room allocated; key re-issued; guest is informed.
  • Offline window exceeded. UI shows a hard banner: "Sync overdue — please contact support." All reads still work; mutations are blocked until reconnect to prevent ungovernable divergence.
  • Voltage spike crash mid-flow. Local SQLite uses WAL + synchronous=NORMAL; the partial mutation is either complete (committed) or fully rolled back; the encoded card status is read at boot to detect orphans.

Variants.

  • Cash + card (deposit + final). A two-line folio with one cash + one card payment; sync handles each independently.
  • Multi-night walk-in. Same flow with extended stay window.
  • Group walk-in routes to J-10 with the offline path applied per reservation.

ASCII sequence diagram.

[OFFLINE]
Front Desk Clerk Electron Desktop (Renderer) Electron Main / Local Sidecar SQLite Local Encoder Agent Outbox Queue
│ │ │ │ │ │
walk-in guest │ │ │ │ │
├──────────────────────► │ open WalkInDrawer │ │ │ │
│ enter guest + dates │ │ │ │ │
├──────────────────────► │ POST /local/reservations │ │ │ │
│ ├──────────────────────────────────────►│ create provisional │ │ │
│ │ ├─────────────────────────►│ INSERT reservation │ │
│ │ │ │ INSERT folio (open) │ │
│ │ │ ────────────────────────────────────────────────────────────────────────────►│ enqueue: reservation.held.v1, reservation.confirmed.v1
│ take cash │ │ │ │ │
├──────────────────────► │ POST /local/payments │ │ │ │
│ ├──────────────────────────────────────►│ post cash payment │ │ │
│ │ ├─────────────────────────►│ INSERT folio entry │ │
│ │ │ ────────────────────────────────────────────────────────────────────────────►│ enqueue: billing.payment.posted.v1
│ encode card │ │ │ │ │
├──────────────────────► │ POST /local/key-credentials │ │ │ │
│ ├──────────────────────────────────────►│ mint credential │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ uses offline-cert │ │ │
│ │ ├─────────────────────────────────────────────────►│ encode RFID │
│ │ │ ◄── encodedAt, credentialId ─────────────────── │ │
│ │ │ INSERT credential (provisional) │ │
│ │ │ ────────────────────────────────────────────────────────────────────────────►│ enqueue: key_credential.issued.v1 (provisional)
│ hand over key │ │ │ │ │
│ ◄── done ────────────────────────────────────────────────────── │
[--- RECONNECT @ 06:00 ---]
│ Sync Engine: pull delta, push outbox in order, reconcile credentials │

Telemetry. frontend.walkin.created_offline · frontend.walkin.key_encoded_offline · frontend.sync.outbox_flushed { count } · frontend.sync.conflict_surfaced { kind } · KPI: sync conflict rate (target ↓).


J-08 — Online Check-In Queue Coordination

Goal. Six guests are due to arrive between 14:00 and 14:30. The front-desk clerk uses the ArrivalsBoard to prioritise, marks two rooms as "ready in 15 min" so the guests can be seated, batches mobile-key invitations to reduce wait.

Persona / Surface. Front Desk · Electron Desktop.

Preconditions.

  • Reservations in state confirmed; check-in window opens at the property's policy time.
  • Mobile-key invitation requires guest's phone number on file.

Trigger. Front desk sees the arrivals queue building.

Steps.

  1. Open ArrivalsBoardUI: shows arrivals grouped by ETA (within 30 min, 30–60 min, after 60 min). Each card shows guest, room, status (Room Ready / Cleaning / Inspection), VIP/special-request flags, payment posture (Pre-paid / On Arrival), and a quick CTA. · System: live data via SSE channel from bff-backoffice-service. · Data: read-only.
  2. Mark "ready in 15 min"UI: on a row whose room is dirty, clerk can tap ETA 15m to set a soft promise. · System: housekeeping-service BumpPriorityUseCase is called; the housekeeping board surfaces a high-priority signal. · Events: melmastoon.housekeeping.task.priority_bumped.v1.
  3. Batch mobile-key invitesUI: clerk multi-selects 4 reservations whose rooms are clean and clicks Send mobile keys. · System: lock-integration-service issues credentials in parallel (one per reservation); notification-service sends batched SMS/WA. · Events: melmastoon.lock_integration.key_credential.issued.v1 (× N), melmastoon.notification.message.queued.v1 (× N).
  4. Guests arriveUI: clerk taps Mark arrived on each row; if guest already used the mobile key, the system auto-completes check-in based on the lock telemetry (lock_event.unlocked.v1) but still requires HITL ID verification at the desk per tenant policy.
  5. Audit trail. Each priority bump and each mobile-key invite is logged.

Success criteria.

  • No guest waits > 10 minutes in the lobby for a room that was promised "ETA 15 min".
  • 90% of mobile-key invites delivered within 30s.
  • Front desk avoids context-switching across single-row screens.

Failure paths.

  • Mobile-key SMS delayed. Visible per-row badge "Sent — pending"; auto-retry; clerk can re-send manually after 60s.
  • Housekeeping does not deliver on ETA. Board flips to "Late" badge; clerk can escalate to housekeeping lead via a single tap (notification through notification-service).
  • Lock telemetry stale. Auto-complete is not used; manual completion only.

Variants.

  • Express check-in (Phase 2) skips the ID step if pre-collected during booking.
  • Loyalty / VIP routes the row to a manager-styled affordance.

Telemetry. frontend.arrivals.eta_set { minutes } · frontend.arrivals.mobile_keys_batched { count } · frontend.arrivals.late_alert · KPI: time-in-lobby p95.

Detailed branch behaviour. The ArrivalsBoard is intentionally one of the densest screens in the desktop app. Each row exposes inline progressive disclosure rather than navigating away:

  • Row collapse / expand reveals special requests, ID status, payment posture, AI upsell suggestions (e.g., "Suggest deluxe upgrade — guest historically books deluxe"), and any sync-pending mutations.
  • Bulk select toolbar appears when any row is selected; supports Send mobile keys, Send arrival reminder, Mark arrived, Reassign room, Print registration cards.
  • Filter chips at the top: VIP, Pre-paid, On Arrival, Foreign national, Has special request, Awaiting room ready. Filters are tenant-customisable via bff-backoffice-service presets.
  • Time-banded sub-headers ("Within 30 minutes", "30 – 60 minutes", "After 60 minutes", "Late") repaint as time advances; "Late" rows turn amber after 15 minutes past expected arrival and prompt a "Reach out" CTA that drafts a multilingual message via ai-orchestrator-service for HITL approval.

The board also receives lock-event hints — a row whose lock_event.unlocked.v1 arrives flips its arrival pill to "Self-arrived" (the guest used the mobile key) and tags the row for ID verification at the next desk encounter.


J-09 — Mid-Stay Issue: Lock Battery Dies

Goal. Mid-afternoon, a guest can't enter their room. The front desk sees an alert, issues a replacement RFID card, logs a maintenance work order, and confirms the guest is back in the room.

Persona / Surface. Front Desk + Maintenance · Electron Desktop.

Preconditions.

  • lock-integration-service device-health probes are running; battery telemetry threshold is configured per vendor.
  • Reservation is checked_in.

Trigger. Guest reports failure or lock_integration.device.health_alert.v1 event arrives.

Steps.

  1. Alert in Notifications feedUI: a high-priority alert on the desktop "Lock 304 — battery critical". · System: lock-integration-service ProbeDeviceHealthUseCase emits the alert. · Events: melmastoon.lock_integration.device.health_alert.v1.
  2. Replace credentialUI: clerk opens the reservation, taps Replace key. · System: lock-integration-service ReplaceKeyCredentialUseCase revokes the old credential and issues a new one (RFID card encoded on-prem; mobile key reissued). · Events: melmastoon.lock_integration.key_credential.revoked.v1, melmastoon.lock_integration.key_credential.issued.v1.
  3. Log maintenance work orderUI: clerk taps Create work order on the alert. · System: maintenance-service CreateWorkOrderUseCase with priority = high, asset = Lock-304. · Events: melmastoon.maintenance.work_order.created.v1.
  4. Hand over new key, escort if needed.
  5. Maintenance completesSystem: battery replaced; maintenance-service CompleteWorkOrderUseCase; lock health probe confirms green within 5 minutes.

Success criteria.

  • Replacement credential issued within 2 minutes of the alert.
  • No reaccommodation needed for an in-house guest in this flow.
  • Maintenance ticket closes within tenant SLA (default 4h for high priority).

Failure paths.

  • Vendor outage prevents revoke. The credential is revoked in the local SQLite mirror with a deferred reconciliation; lock telemetry will block the old card on next contact; physical re-keying is the worst-case fallback.
  • No spare RFID. The pre-encoded backup pool is exhausted; clerk reissues a one-time PIN via the lock vendor's PIN feature where available; otherwise mechanical override.
  • Maintenance not available (off-shift). The alert routes to the GM with an escalation timer.

Variants.

  • Multiple rooms affected (low-battery batch alert). The alert lists all affected rooms; clerk can mass-replace.

Telemetry. frontend.lock.alert_acknowledged · frontend.lock.credential_replaced · frontend.maintenance.work_order_created · KPI: lock incident MTTR.

Vendor-specific behaviour. The lock-integration-service adapters expose subtly different failure shapes that the front-desk experience has to absorb without the operator needing to know which vendor they are using:

  • TTLock — cloud API; battery telemetry is reported on each lock event. A revoke is acknowledged in 1–3s when online; if the lock has not contacted the cloud in > 24h, the new credential is queued and re-tried until the lock next checks in. The fallback is a one-time PIN issued via the lock's keypad mode.
  • Salto — Salto Connect cloud + on-prem encoder hybrid. Revoke is near-instant for online locks; for offline locks (Salto SVN), the new key carries the revocation in its access plan; the next time the old card is presented, the lock blocks it. The desktop's "Replace key" flow encodes the new card on-prem in either case.
  • Assa Abloy — typically on-prem encoder with cloud sync; same offline-friendly model as Salto SVN.
  • Generic Wiegand / RFID — purely on-prem; revoke is instantaneous in the local controller; vendor cloud is not in the loop.

The desktop normalises all of this behind a single Replace key affordance with a contextual "Vendor: · Online" badge, and the operator never needs to know the underlying differences.


J-10 — Group Booking with Multiple Rooms & Single Folio

Goal. A wedding party of 8 rooms has one payer (the groom's father). The front desk creates a group hold, allocates 8 rooms atomically, manages a single shared folio with sub-charges, and produces a group invoice at checkout.

Persona / Surface. Front Desk · Electron Desktop.

Preconditions.

  • Tenant has group-booking enabled; group rate plan exists in pricing-service.
  • Sufficient inventory in the requested window.

Trigger. Group inquiry walks in or arrives via a tenant-side group request.

Steps.

  1. Create group holdUI: GroupBookingDrawer collects party name, dates, # rooms by type, payer details, special requests (welcome flowers, event-night dinner block). · System: reservation-service CreateGroupHoldUseCase creates a GroupHold aggregate that fans out into 8 reservation holds, all linked to a groupId and a single groupFolio. · Events: melmastoon.reservation.group_hold.created.v1, melmastoon.inventory.allocation.committed.v1 (× 8), melmastoon.billing.folio.opened.v1 (group folio).
  2. Apply group rate + depositUI: deposit field (e.g., 30%); payer selects card or cash. · System: billing-service opens the group folio with the deposit charge; payment-gateway-service processes the deposit payment; remaining balance is due_at_checkout.
  3. Per-guest details capture (later)UI: clerk can fill guest names per room incrementally; AI transliteration helps; some names may be "TBD" until arrival.
  4. Arrivals — same as J-05 per reservation, but with a "Group: " pill across the cards.
  5. Mid-stay charges — mini-bar / restaurant charges are posted to the group folio with a room attribute for sub-totaling. · System: billing-service PostChargeUseCase with folioRef = group.
  6. Group checkout & invoiceUI: GroupCheckOutDrawer shows the consolidated folio with per-room sub-totals. Final payment is collected from the payer. · System: group folio is sealed; an invoice PDF is rendered with the full breakdown.

Success criteria.

  • The 8 holds either all succeed or all fail (atomic at the inventory level using a saga with compensating release).
  • Mid-stay charges roll up correctly per-room and across the group.
  • One invoice PDF reconciles to the sum of all sub-charges.

Failure paths.

  • Partial inventory failure. The compensating step releases all holds; UI offers "Try X rooms instead" or "Wait for similar room types".
  • Payer disputes a sub-charge (J-11 routed for that line item only).
  • Split-folio mid-flow (party splits the bill). A sub-flow forks the group folio into per-room folios; financial entries are moved with audit trails (manager approval required).

Variants.

  • Corporate group with a different rate plan and consolidated tax invoice format.
  • Allotment / contracted block (Phase 2) where the inventory is pre-blocked vs an external organisation.

Telemetry. frontend.group.created { rooms } · frontend.group.deposit_collected · frontend.group.checkout_complete · KPI: avg group revenue, group conversion.


J-11 — Late Checkout & Folio Dispute

Goal. A guest claims that a mini-bar charge is wrong. The front desk reviews the folio audit log, applies a credit note (with manager override), updates the invoice, and sends the corrected version to the guest.

Persona / Surface. Front Desk + Manager · Electron Desktop.

Preconditions.

  • Reservation is in check_out_started (or already checked_out with a re-open flow).
  • Manager has role manager with folio.adjust_with_override.

Trigger. Guest disputes a charge at the desk.

Steps.

  1. Review folio audit logUI: FolioAuditLog lists every entry with actor, timestamp, source (system, manual, OTA), evidence (photo/scan/POS reference if any). · System: read from billing-service projection.
  2. Apply credit noteUI: clerk enters the disputed amount and reason; the action requires manager override (PIN or biometric). · System: billing-service PostCreditNoteUseCase writes a new immutable folio entry that offsets the disputed charge; original charge remains for audit. · Events: melmastoon.billing.credit_note.posted.v1.
  3. Re-issue invoiceUI: clerk taps Regenerate invoice. · System: billing-service produces a new versioned invoice PDF; notification-service sends to the guest's preferred channel. · Events: melmastoon.billing.invoice.issued.v1.
  4. Complete checkout — proceeds as J-06.

Success criteria.

  • All adjustments require manager override and are immutably logged.
  • The final invoice always equals the cumulative ledger sum (no out-of-band edits).
  • Disputes are closed within an SLA the tenant configures (default 30 minutes at the desk).

Failure paths.

  • Manager unreachable. A "Hold dispute" affordance pauses checkout; guest can leave with a "Pending dispute" receipt; resolution happens within 24h.
  • Refund needed (charge already paid). payment-gateway-service RefundUseCase runs; the refund is asynchronous; invoice is marked "Refund pending" until provider confirms.

Variants.

  • Partial refund (only part of the charge disputed) — same flow with a partial credit note.
  • Re-opened folio (post-checkout dispute) — billing-service ReopenFolioUseCase requires GM approval; the rule is policy-driven and audit-flagged.

Telemetry. frontend.folio.dispute_started · frontend.folio.credit_note_applied { amount, reason } · frontend.folio.invoice_reissued · KPI: dispute resolution time, refund cycle time.

Audit posture. Every dispute resolution emits a paired pair of events: the offsetting credit_note.posted.v1 and a melmastoon.audit.folio.adjusted.v1 audit anchor that captures the actor chain (clerk + manager override), the precise charge being offset, the reason text, and a policyOverride flag. These are persisted into the immutable audit log per docs/07 and are visible to the tenant's Compliance Officer in the platform staff control plane (read-only across tenant boundaries with explicit consent). Dispute trends per clerk are aggregated by analytics-service and surface on the GM dashboard (J-16) as a "training opportunity" signal — never as a punitive metric.


J-12 — End-of-Day Cash Drawer Close

Goal. At shift end, the front desk closes the drawer, counts cash, reconciles vs system totals, secures two-staff sign-off if variance exceeds tolerance, generates the EOD report, and emails it to the GM.

Persona / Surface. Front Desk + Finance · Electron Desktop.

Preconditions.

  • Shift is open in staff-service; drawer was opened with a starting float.
  • Tenant has tolerance configured (default ±$5 USD-equivalent).

Trigger. Front desk taps Close shift.

Steps.

  1. Open close-shift dialogUI: CloseShiftDialog shows expected cash (system-computed = opening float + cash payments collected − cash refunds − cash drops to safe).
  2. Count cashUI: clerk enters counted amount per denomination; the dialog auto-totals.
  3. Variance checkSystem: if |counted − expected| ≤ tolerance, accept and proceed. If > tolerance, the dialog requires a second-staff PIN/biometric sign-off and a free-text reason (e.g., "found $5 short, suspected lost change at 14:00 transaction").
  4. Settlement recordSystem: billing-service SettleShiftUseCase creates an immutable settlement with expected, counted, variance, reason, signedBy[]. · Events: melmastoon.billing.shift.settled.v1.
  5. EOD report generationSystem: reporting-service runs the EOD report (occupancy, ADR, RevPAR, channel mix, payment mix, no-shows, cancellations, complimentary, variance). · Events: melmastoon.reporting.report.generated.v1. The PDF is emailed via notification-service to the GM and any subscribers.
  6. Drawer lockedUI: drawer status flips to closed; the next shift open requires a new opening float.

Success criteria.

  • 100% of shifts produce a settlement record (no orphan shifts).
  • EOD email arrives within 5 minutes of close.
  • Variance > tolerance always produces a two-person sign-off.

Failure paths.

  • Offline at close. The settlement record is stored locally with status = pending_sync; the EOD report is rendered locally from SQLite and emailed at reconnect. The next-shift float can open against the local settlement.
  • Second-staff unavailable for a variance > tolerance. The shift cannot close; a "Hold" status is allowed for up to 4h; an alert escalates to the GM.
  • System-expected vs counted discrepancy is repeated across shifts. analytics-service flags an anomaly; the GM dashboard surfaces "Recurring variance trend" (J-16).

Variants.

  • Multi-currency drawer (USD + AFN simultaneously). The dialog has tabs per currency; variance is computed per currency; settlement record carries both.
  • Cash drop to safe mid-shift is a separate event (melmastoon.billing.cash_drop.recorded.v1) and reduces expected cash at close.

Telemetry. frontend.eod.close_clicked · frontend.eod.variance_observed { amount } · frontend.eod.signed_off_by_two · melmastoon.billing.shift.settled.v1 · KPI: end-of-day variance, % shifts requiring double sign-off.


5. Management Journeys (J-13 → J-18)

J-13 — Onboard New Tenant (Single Hotel Operator)

Goal. A guesthouse owner signs up, creates their tenant, configures the property, invites staff, customises the theme, publishes the booking site, and accepts their first live booking.

Persona / Surface. Owner · Control plane (web) and Electron Desktop.

Preconditions. Owner has business credentials, KYC documents, and a payment-gateway account or willingness to use cash-on-arrival only.

Trigger. Owner clicks Start with Ghasi Melmastoon on the marketing site.

Steps.

  1. Create tenant — Email + password + phone + business name; KYC capture (uploads via file-storage-service); tenant-service CreateTenantUseCase provisions the tenant with status = pending_review. Platform-staff review (assisted onboarding per 01 §11).
  2. Configure tenant settings — Languages (Pashto, Dari, EN; primary chosen), currency (AFN; secondary USD display), tax model (provincial bed-tax + service charge), check-in/out times, cancellation policy.
  3. Invite staff — Bulk invite GM + 2 front-desk + 2 housekeeping; roles assigned per docs/07. Invitees download the Electron desktop app via auto-update channel.
  4. Configure first propertyproperty-service CreatePropertyUseCase; property name, address, geo (used by meta layer), photos, amenities, policies.
  5. Create room types and roomsRoomTypeUseCase (king, twin, suite); CreateRoomUseCase per physical room.
  6. Upload photos — Per property + per room; processed by file-storage-service (resize, optimize, AVIF/WebP).
  7. Set up rate planspricing-service CreateRatePlanUseCase for BAR, weekly, government, corporate; configure restrictions (MinLOS, CTA, CTD).
  8. Customise themetheme-config-service: pick layout preset, set tokens (colors, typography), add content blocks (about, policies, FAQs), set RTL/LTR defaults.
  9. Preview — Renders the tenant booking site under a staging subdomain.
  10. Publishtheme-config-service PublishThemeUseCase; the tenant booking site goes live; search-aggregation-service indexes the property; the meta layer starts surfacing it.
  11. First live booking — confirmed via J-03/J-04.

Success criteria.

  • Tenant onboarding completes in ≤ 2 hours of operator time across staff invitation and theme configuration.
  • The first booking is processed end-to-end without engineering intervention.
  • All AI-suggested tokens (e.g., suggested theme palette from logo via ai-orchestrator-service) are HITL-confirmed.

Failure paths.

  • KYC rejected. Tenant remains pending_review with a clear remediation message.
  • Theme accessibility regression (contrast ratio fails). theme-config-service blocks publish; suggests a remediation; logs the event.
  • Geo lookup fails. Manual lat/lon entry with a map picker.

Variants.

  • Single-property (this flow) vs chain (J-14).

Telemetry. tenant.onboarding.completed { duration_minutes } · tenant.theme.published · tenant.first_booking_confirmed · KPI: provider-onboarding time.

Onboarding wizard structure. The onboarding experience is a 5-stage wizard surfaced both in the control plane (web) and the desktop app's Welcome tab:

  1. Account & business — owner identity, business name, tax IDs, KYC docs.
  2. Property & rooms — at least one property; room types; rooms; amenities; geo.
  3. Pricing & policies — at least one rate plan (BAR); cancellation policy; check-in/out times; tax model.
  4. Theme & content — layout preset; tokens (logo, colors, type ramp); content blocks (about, policies, FAQs); locales.
  5. Go-live checklist — automated pre-flight checks (theme accessibility, photos uploaded, rate plan published, payment method configured, staff invited, lock vendor connected if applicable). Each item green/amber/red; cannot publish until all critical items are green.

Assisted onboarding. Phase 0/1 onboarding is assisted by a Ghasi platform-staff onboarding specialist. The control plane exposes an "Open with operator" affordance which screen-shares the tenant's onboarding state for live coaching; all such sessions are audit-logged with explicit tenant consent.

AI-assisted setup. Optional ai-orchestrator-service calls assist with: theme palette generation from a logo (palette suggestion with WCAG-validated contrast checks); FAQ block drafting from the property's existing website; description re-translation across enabled locales. All AI artefacts carry provenance and require HITL confirm before publish.

First-booking watchdog. A scheduled job watches new tenants and surfaces a "Help configure first booking" affordance to the platform onboarding specialist if the tenant is published but no booking has confirmed within 14 days. This is a Phase 0 retention lever; metrics feed the activation funnel dashboard in the platform staff control plane.


J-14 — Onboard Chain (Multi-Property Tenant)

Goal. A regional chain operator onboards 5 properties under one tenant, with an org-unit hierarchy (chain → region → property), per-property staff, shared chain branding overrideable per property, and cross-property reservation visibility for chain operators.

Persona / Surface. Chain Operator · Control plane + Electron Desktop.

Preconditions. Tenant is created and approved per J-13.

Trigger. Chain operator selects "Add region/property" from the tenant settings.

Steps.

  1. Create org-unit hierarchytenant-service CreateOrgUnitUseCase (chain → region → property nodes); permissions cascade via attribute-based access control.
  2. Add propertiesproperty-service CreatePropertyUseCase × 5 within the chain; each carries the parent org-unit reference.
  3. Assign staff — Per-property staff get role-scoped access; chain-level GMs get cross-property access via attribute filter (orgUnitId IN (region IDs)).
  4. Configure shared chain brandingtheme-config-service chain-level theme (master tokens). Per-property overrides allowed within governed bounds (logo, photos, content blocks); core color tokens locked.
  5. Cross-property reservation visibility — Chain operator's role grants reservation.read across all properties; UI surfaces a property switcher and a "Cross-property arrivals" board.
  6. Reporting consolidationreporting-service supports chain rollups (occupancy, ADR by property, channel mix consolidated).

Success criteria.

  • ABAC scoping is correct: a property GM cannot see another property's reservations.
  • Theme inheritance works: changing the chain's primary color updates all properties unless overridden.

Failure paths.

  • Permission misconfiguration flagged by docs/07 automated checks; surfaced as a blocking warning.
  • Attempt to override locked tokens at property level rejected with explanation.

Variants.

  • Brand-family within chain (Phase 2): an additional layer in the org-unit hierarchy.

Telemetry. tenant.chain.org_units_created { count } · tenant.chain.cross_property_view_opened · KPI: standardisation compliance score (theme overrides ≤ tenant policy).

Org-unit hierarchy semantics. tenant-service models the hierarchy as a closure-table with explicit edge types (owns, manages, audits). Each membership is scoped to one or more org-units; permissions resolve at request time by walking the closure with attribute-based access control filters. The desktop's Property switcher respects the closure: a regional GM sees properties under their region only; a chain GM sees the full chain; a property GM sees only their property.

Theme inheritance. The theme-config-service supports a themePolicy per chain that declares which tokens are locked at chain level (cannot be overridden), which are recommended but overrideable, and which are property-only. The desktop's Theme editor renders locked tokens with a chain-icon and an explanatory tooltip; attempting to edit raises a structured error mapped to a friendly message in the operator's locale.

Cross-property reservation visibility. The arrivals board has a "Cross-property" toggle for chain-level roles; the data is fetched with the same RLS-aware query patterns; row colors encode the originating property; clicking a row deep-links to the per-property context (with a breadcrumb back to the cross-property view). Sync semantics for chain-level operators are identical — the desktop subscribes to all org-units the user can see and reconciles against the local SQLite per-property partitions.

Reporting. Chain-level reports include a per-property dimension by default; the consolidated view aggregates across the chain with FX normalisation to the chain's reporting currency (configurable). Per-property invoices remain tax-jurisdiction-correct.


J-15 — Configure Dynamic Pricing AI Suggestions

Goal. A GM enables AI dynamic-pricing suggestions for the next 30 days, configures safety floor/ceiling per room type, reviews the first batch, accepts some, modifies others, rejects some — and the system learns the GM's acceptance pattern.

Persona / Surface. GM · Electron Desktop.

Preconditions.

  • ai-orchestrator-service is healthy and connected to Vertex AI cloud models.
  • pricing-service RatePlan aggregates have a dynamic_suggestions_enabled flag set per rate plan.

Trigger. GM opens Settings → Dynamic Pricing.

Steps.

  1. Enable per rate planUI: toggles per rate plan with safety bounds (floor/ceiling per room type, min vs max % delta vs BAR). · System: pricing-service UpdateRatePlanUseCase; emits melmastoon.pricing.rate_plan.changed.v1.
  2. First batch suggestions arriveSystem: ai-orchestrator-service SuggestRatesUseCase runs on a schedule (daily at a configurable time); produces { date, roomTypeId, suggestedRate, baseRate, rationale, confidence, provenance }. Suggestions are queued in the GM's "AI Inbox". · Events: melmastoon.ai_orchestrator.suggestion.served.v1.
  3. ReviewUI: AISuggestionsInbox shows each suggestion with rationale ("Forecast occupancy 92% on Friday; market signal +12%"), provenance badge (model, version, prompt ID), and three CTAs: Accept, Modify, Reject.
  4. Accept some / modify others / reject someSystem: per-suggestion action recorded; on Accept or Modify, pricing-service ApplyDynamicAdjustmentUseCase updates the rate (HITL-attested); audit log records actor, provenance, action. · Events: melmastoon.pricing.dynamic_adjustment.applied.v1.
  5. Learning loopai-orchestrator-service ingests the action history; downstream batches reflect the GM's preferences.

Success criteria.

  • Every applied rate change is HITL-attested with provenance.
  • Acceptance rate trends upward over 4 weeks (target).
  • No rate violates the safety floor/ceiling.

Failure paths.

  • AI provider rate-limited / model degraded. "Suggestions unavailable today" affordance; previously accepted rates remain in effect.
  • Safety bounds violated by a suggestion (rare; defensive). The suggestion is filtered out and logged for model improvement.
  • GM bulk-rejects systematically. A telemetry signal alerts the AI team to retrain or revisit prompts.

Variants.

  • Auto-apply (Phase 2 only, gated by readiness criteria in docs/08) — irreversible-action HITL guardrails still apply per the AI HITL bypass = 0 KPI.

Telemetry. frontend.ai.suggestion_viewed { id } · frontend.ai.suggestion_action { action: "accept|modify|reject" } · melmastoon.pricing.dynamic_adjustment.applied.v1 · KPI: AI suggestion acceptance rate, AI HITL bypass attempts (= 0).

Suggestion lifecycle in detail. Each AI suggestion has a deterministic identifier (suggestionId) and goes through five observable states:

  1. Generated by the AI orchestrator (cloud Vertex AI for forecasting + heuristic blend; ONNX edge on offline desktop falls back to a smoothed last-week baseline).
  2. Served to the GM's inbox (suggestion.served.v1).
  3. Reviewed when the GM opens it (suggestion.viewed).
  4. Acted on with accept / modify / reject (suggestion.acted.v1).
  5. Applied if accepted (or modified-and-saved) — produces a pricing.dynamic_adjustment.applied.v1 event with provenance.

Every state transition is timestamped and contributes to the AI evaluation log per docs/08. Modified suggestions feed the model's preference signal: if a GM systematically reduces the suggested rate by 5%, the next batch's prompt context includes that priors hint. There is no implicit auto-apply ever in Phase 0/1; Phase 2 may introduce per-surface auto-apply with a hard tenant opt-in and an irreversible-action HITL guard that remains.


J-16 — Review Daily Operations Dashboard

Goal. The GM opens the dashboard at 08:00, reviews occupancy, ADR, pickup vs forecast, AI suggestions, alerts, and the recent activity feed; drills into one alert (a no-show pattern flagged by AI), and takes action.

Persona / Surface. GM · Electron Desktop.

Preconditions. Reservations and EOD data from prior days are projected into analytics-service.

Trigger. GM opens the desktop app.

Steps.

  1. Dashboard loadsUI: OperationsDashboard shows occupancy (today / next 7 days), ADR, RevPAR, pickup vs forecast, top AI suggestions, alerts, recent activity. · System: bff-backoffice-service aggregates from analytics-service projections; SSE stream for new events.
  2. Drill into alertUI: "AI flagged: 3 no-shows on Tuesdays from agent-channel bookings". · System: ai-orchestrator-service DetectAnomaliesUseCase returned this with provenance and supporting data.
  3. Take actionUI: GM opens the suspect agent's bookings; cross-checks; chooses to require deposit for that channel going forward via pricing-service rate-plan policy. · System: policy update; melmastoon.pricing.rate_plan.changed.v1.
  4. Notify staff — GM clicks "Send to staff briefing"; notification-service posts to the team channel.

Success criteria.

  • Dashboard loads in ≤ 2s p95.
  • Alerts surface relevant, recent context with provenance for any AI-driven items.
  • Drill-downs answer the "why" without leaving the desktop.

Failure paths.

  • Stale projection. Dashboard shows a "Last refresh: 3m ago" badge; auto-refresh on focus.
  • AI insight unsupported (provider down). "AI insights unavailable" state; non-AI metrics still render.

Variants.

  • Chain GM sees aggregated views (J-14) with property switcher.

Telemetry. frontend.dashboard.opened · frontend.dashboard.alert_clicked { kind } · frontend.dashboard.action_taken · KPI: alert response time.

Information density. The OperationsDashboard is built around six tile categories — Today, Pickup, Suggestions, Alerts, Activity, Health. Each tile is independently refreshable; the user can pin and reorder. Tiles consume different update channels: Today and Pickup read warm aggregates from analytics-service, Suggestions from ai-orchestrator-service, Alerts from a fanout SSE stream backed by audit and event subscriptions, Activity from the per-tenant event tail, Health from per-service status reported by bff-backoffice-service. The dashboard's first paint is sourced entirely from local SQLite snapshots; the SSE stream then upgrades the view in place.


J-17 — Run Monthly Tax Report

Goal. Finance opens reporting, selects "Monthly Tax Report" for the previous month, applies a property filter, downloads PDF + Excel, reviews tax breakdown by jurisdiction, submits to the tax authority.

Persona / Surface. Finance · Electron Desktop.

Preconditions. Folios for the period are sealed; tax model is configured per tenant/property.

Trigger. Finance opens Reports → Monthly Tax Report.

Steps.

  1. Select report + filtersUI: ReportPicker; period = last month; property = selected. · System: reporting-service GenerateReportUseCase queues the job. · Events: melmastoon.reporting.report.requested.v1.
  2. GenerationSystem: job runs; reads from billing-service and analytics-service; produces PDF + Excel; stores in file-storage-service. · Events: melmastoon.reporting.report.generated.v1.
  3. DownloadUI: download link; offline cache for 30 days.
  4. Review — Finance verifies tax lines per jurisdiction (provincial bed-tax, service charge, VAT if applicable).
  5. Submit — Out-of-band per local procedure.

Success criteria.

  • Report generated in ≤ 5 minutes for a typical tenant scale.
  • Tax breakdown matches the immutable folio ledger to the cent.
  • Excel is auditor-friendly (clear columns, totals, footnotes).

Failure paths.

  • Job timeout (very large dataset). Job retries with chunked generation; user sees progress.
  • Currency mismatch (multi-currency tenant). Report shows per-currency sections with FX-snapshot footnotes.

Variants.

  • Multi-property report (chain) — same flow with property dimension.

Telemetry. frontend.reports.requested { type } · frontend.reports.downloaded { type } · KPI: report generation time.

Report catalogue. reporting-service ships twelve canonical reports: Occupancy, ADR, RevPAR, GPAR, Channel Mix, Payment Mix, Daily EOD, Audit Log, Monthly Tax, Daily Guest Registration, Cancellation Reasons, Forecast vs Actual. Each has a JSON schema, a PDF template, and an Excel template. The desktop renders previews of recent reports inline; PDFs and Excels download via signed URL from file-storage-service.

Scheduling. Reports can be scheduled (daily / weekly / monthly) with delivery via email + SMS link; scheduled jobs execute on the cloud reporting-service worker pool and respect tenant timezone. The desktop's Reports tab shows past, scheduled, and ad-hoc reports in one list with status badges.


J-18 — Submit Daily Guest Registration to Authority

Goal. The receptionist generates the daily guest-registration report (mandatory in Afghanistan and several target markets), reviews flagged guests (foreign nationals require additional info), and either e-files or prints + delivers per local procedure.

Persona / Surface. Receptionist or Admin · Electron Desktop.

Preconditions.

  • Tenant policy has the guest-registration report enabled.
  • All checked-in guests have ID captured per J-05 step 3.

Trigger. End of day or on-demand by request from authority.

Steps.

  1. Generate reportUI: Reports → Guest Registration (Daily). · System: reporting-service job pulls all in-house guests + arrivals + departures with ID fields. · Events: melmastoon.reporting.report.generated.v1.
  2. Flagged guestsUI: foreign nationals are flagged; missing fields highlighted (e.g., visa expiry).
  3. Fill missing fieldsUI: clerk completes from passports; updates persisted to Reservation.guests[].
  4. SubmitUI: either E-file (if a regional integration is configured) or Print + deliver. · System: on e-file, an outbound integration adapter handles transmission with retry; on print, the PDF is generated and a courier signature can be captured.

Success criteria.

  • 100% of in-house guests appear on the report; no orphaned check-ins.
  • Submission is logged immutably.

Failure paths.

  • Missing data blocking submission. Clerk is guided to fill the gaps; report cannot submit until complete.
  • E-file integration outage. Falls back to print + deliver; submission logged with method.

Variants.

  • Multiple authorities (Phase 1 markets) — multiple destinations per submission.

Telemetry. frontend.compliance.daily_report_generated · frontend.compliance.daily_report_submitted { method } · KPI: compliance submission timeliness.

Submission methods. The reporting-service exposes a pluggable SubmissionPort with adapters: e-file (per-jurisdiction REST/SFTP), email-with-PDF, print-and-deliver (generates a PDF + a courier-signed acknowledgement form). Per market, the tenant configures which method is allowed; in Phase 0 (Afghanistan) most provinces require print-and-deliver, while Iran requires e-file via a national portal that reporting-service integrates with through a dedicated adapter. Failed submissions never silently disappear — the desktop's Compliance tab surfaces a list of pending submissions with retry affordances and a "Mark as delivered out-of-band" override (audit-flagged, manager approval).


6. Housekeeping Journeys (J-19 → J-22)

J-19 — Auto-Cleaning on Checkout (Online)

Goal. A guest checks out at 11:00. A housekeeping task is auto-created with priority based on the next reservation's arrival time; AI suggests assignment to the nearest available housekeeper; the lead approves; the housekeeper is notified, completes the task, the lead inspects, and the room returns to ready.

Persona / Surface. Housekeeping Lead + Housekeeper · Electron Desktop.

Preconditions.

Trigger. melmastoon.reservation.checked_out.v1 event.

Steps.

  1. Auto-create taskSystem: housekeeping-service ReservationCheckedOutHandler invokes CreateTaskUseCase with kind = turnover, priority derived from next_reservation.eta (within 4h → high; within 1h → urgent). · Events: melmastoon.housekeeping.task.created.v1.
  2. AI suggests assignmentSystem: RoutingSuggestionPort.suggestAssignments calls ai-orchestrator-service; returns ranked list of (housekeeper, ETA, distance from current floor). · Events: melmastoon.ai_orchestrator.suggestion.served.v1.
  3. Lead approvesUI: HousekeepingBoard shows the new task with the AI suggestion pre-filled; lead taps Assign. · System: AssignTaskUseCase. · Events: melmastoon.housekeeping.task.assigned.v1.
  4. Housekeeper notifiedSystem: notification-service sends in-app push (if app-equipped) or SMS (for staff without app); large-touch UI for the housekeeper.
  5. Start taskUI: housekeeper taps Start. · System: StartTaskUseCase; room_status = cleaning. · Events: melmastoon.housekeeping.task.started.v1.
  6. Complete task — Housekeeper completes checklist (linen, toilet, restock, dust, vacuum); taps Done. · System: CompleteTaskUseCase. · Events: melmastoon.housekeeping.task.completed.v1.
  7. InspectionUI: lead opens InspectionDrawer, runs through checklist (Phase 1: lightweight; Phase 2: photo capture). · System: RunInspectionUseCase; on pass, room_status = clean (ready). · Events: melmastoon.housekeeping.inspection.passed.v1, melmastoon.housekeeping.room.ready.v1.
  8. Arrivals board updates — Front-desk arrivals view (J-08) sees the room flip to "ready".

Success criteria.

  • Auto-task created within 5s of checkout.
  • AI assignment suggestion in ≤ 2s.
  • Time-to-ready meets tenant SLA (default 45 min for standard rooms).

Failure paths.

  • No housekeeper on shift. UI surfaces an alert; lead can pull a cross-shift assist or the GM is paged.
  • AI suggestion unavailable. Lead assigns manually; no flow blockage.
  • Inspection fails. Task status becomes rework; housekeeper notified; SLA timer pauses (configurable).
  • Maintenance issue found (J-22).

Variants.

  • Stayover cleaning (mid-stay) — same flow with kind = stayover.
  • Deep cleaning scheduled tasks not triggered by checkout.

ASCII sequence diagram.

reservation-service Pub/Sub housekeeping-service ai-orchestrator-service Lead (Desktop) Housekeeper (mobile/SMS) notification-service Front-Desk Arrivals
│ │ │ │ │ │ │ │
checked_out.v1 ───────────────────► │ │ │ │ │ │
│ │ ─────────────────────────► │ ReservationCheckedOutHandler │ │ │ │ │
│ │ │ CreateTaskUseCase │ │ │ │ │
│ │ ◄────────────── task.created.v1 ──────────────────── │ │ │ │ │
│ │ │ RoutingSuggestionPort ──────────►│ suggestAssignments │ │ │ │ │
│ │ │ ◄──────────────── suggestion ────│ │ │ │ │
│ │ │ task surfaced on Board ────────────────────────────────────────────►│ │ │ │ │
│ │ │ │ │ Assign clicked │ │ │ │
│ │ │ ◄──────────── AssignTaskUseCase ────────────────────────────────── │ │ │ │ │
│ │ ◄──── task.assigned.v1 ────│ │ │ │ │ │
│ │ │ NotificationPort.notify ─────────────────────────────────────────────────────────────────────────────────────────────────────────────►│ push/SMS to housekeeper │ │
│ │ │ │ │ │ Start clicked │ │ │
│ │ │ ◄──── StartTaskUseCase ────────────────────────────────────────────────────────────────────────────────────────── │
│ │ ◄──── task.started.v1 ─────│ │ │ │ Done clicked │ │ │
│ │ │ ◄──── CompleteTaskUseCase ─────────────────────────────────────────────────────────────────────────────────────────── │
│ │ ◄──── task.completed.v1 ───│ │ │ │ │ │ │
│ │ │ Lead inspects ◄─────────────────────────────────────────────────────│ │ │ │ │
│ │ │ RunInspectionUseCase │ │ │ │ │ │
│ │ ◄──── room.ready.v1 ───────│ │ │ │ │ │ │
│ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────►│ row flips to "Ready" │

Telemetry. housekeeping.task.created · housekeeping.task.ai_suggested { rank, accepted } · housekeeping.task.assigned · housekeeping.task.completed { duration_min } · housekeeping.inspection.passed · housekeeping.room.ready · KPI: turnover SLA hit-rate, rooms ready before check-in window.


J-20 — Offline Housekeeping Update from Desktop

Goal. Internet is down. The lead receives a radio call from housekeeper Mariam: "304 is done." The lead updates the task status on the Electron desktop; later at reconnect, everything syncs.

Persona / Surface. Housekeeping Lead · Electron Desktop (offline).

Preconditions. Local SQLite has the current housekeeping task list; outbox is functional.

Trigger. Verbal/radio update from a housekeeper without app access.

Steps.

  1. Mark task complete (offline)UI: lead opens task 304; taps Mark complete; selects "Reported via radio" reason. · System: local mirror writes status = completed, queues an outbox entry. · Data: housekeeping_tasks row updated locally.
  2. Inspect — Lead inspects room manually; marks inspection passed; outbox queues inspection entry.
  3. Sync at reconnectSystem: outbox flushes in order; server applies idempotent updates; conflicts (rare) surface in Sync Center.

Success criteria.

  • Offline updates round-trip without conflict in ≥ 99% of cases.
  • The housekeeping board reflects the offline-updated state immediately on the local device.

Failure paths.

  • Conflict (server-side recorded a different status, e.g., another lead also marked it complete). Sync Center merges using deterministic precedence (latest server-acknowledged completion wins; reason fields merged); audit log captures the merge.

Variants.

  • Bulk update (e.g., end-of-day reconciliation of multiple radio reports) — multi-select + "Mark all completed".

Telemetry. frontend.housekeeping.offline_update · frontend.sync.outbox_flushed · KPI: sync conflict rate.


J-21 — Mid-Stay Cleaning Request

Goal. A guest requests a linen change via the tenant booking app (Phase 2 includes an in-stay app). The request creates a housekeeping task; a housekeeper is assigned; the task completes; the request is recorded.

Persona / Surface. Guest (mobile/web in-stay app, Phase 2) + Housekeeping · Electron Desktop.

Preconditions. In-stay app access is enabled (a guest deep link active for the stay); reservation is checked_in.

Trigger. Guest taps Request linen change.

Steps.

  1. Submit requestUI: GuestServicesScreen (in-stay app) shows quick options. Guest taps. · System: bff-tenant-booking-service forwards to housekeeping-service CreateTaskUseCase with kind = linen_change, priority = normal, source = guest. · Events: melmastoon.housekeeping.task.created.v1.
  2. Assign + execute — Same as J-19 steps 3–6 minus the post-checkout context.
  3. Notify guest on completionSystem: notification-service sends a brief in-app/SMS confirmation.

Success criteria.

  • Request acknowledged in app within 2 minutes.
  • Task completed within tenant SLA (default 30 min during day shift).

Failure paths.

  • No housekeeper available (busy). Guest sees an ETA; lead can escalate.
  • Guest cancels mid-flight. Task transitions to cancelled; if not started, no impact.

Variants.

Telemetry. frontend.guest.service_request_submitted { kind } · housekeeping.task.created { source: "guest" } · KPI: in-stay request response time.

Privacy. Guest service requests are collected under the reservation's PII scope; staff-facing surfaces show only the room number and the request type, never the guest's full name unless the staff member's role grants it. Requests are auto-purged from analytics 30 days after checkout (per tenant-service retention policy); the underlying audit log retains them per regulatory requirement.


J-22 — Cleaning Reveals Maintenance Issue

Goal. A housekeeper finds a broken air-conditioner during turnover; flags it; the housekeeping task pauses; a maintenance work order is auto-created; the room is transitioned to OOO; if there's an active reservation, the reservation is reaccommodated.

Persona / Surface. Housekeeper + Maintenance · Electron Desktop.

Preconditions. Housekeeper is mid-task; maintenance staff exists.

Trigger. Housekeeper taps Flag maintenance during a task.

Steps.

  1. Flag maintenanceUI: MaintenanceFlagDialog collects asset (AC), severity, optional photo. · System: housekeeping-service RequireMaintenanceUseCase; task transitions to awaiting_maintenance; emits melmastoon.housekeeping.room.maintenance_required.v1.
  2. Auto-create work orderSystem: maintenance-service consumes the event; CreateWorkOrderUseCase with priority derived from severity. · Events: melmastoon.maintenance.work_order.created.v1.
  3. Room → OOOSystem: room state set to OOO via housekeeping-service BlockRoomUseCase; inventory-service consumer reduces availability for the room/dates affected. · Events: melmastoon.housekeeping.room.blocked.v1.
  4. Reaccommodate if neededSystem: if there is an upcoming reservation in the affected window, reservation-service listener offers a same-property alternate room; if none available, surfaces an alert to the GM (J-16) for manual reaccommodation, including communication to the guest in their language.
  5. Maintenance completesSystem: CompleteWorkOrderUseCase; UnblockRoomUseCase; housekeeping task resumes (re-cleaning as needed).

Success criteria.

  • Flag-to-work-order in ≤ 30s.
  • Reaccommodation surfaces within 1 minute of OOO.
  • Guest communication initiated within 5 minutes of GM action.

Failure paths.

  • No alternate room available: GM-led communication with the guest; refund or rebook.
  • Maintenance backlog: SLA breach surfaces on the GM dashboard.

Variants.

  • Routine vs urgent maintenance: SLA differs.
  • Recurring asset issue: analytics-service flags a trend (e.g., "AC failures concentrated on the third floor").

Telemetry. housekeeping.maintenance_flagged { severity } · maintenance.work_order_created · inventory.room_blocked { hours } · KPI: OOS hours avoided, MTTR.


7. Cross-Cutting Journey Themes

7.1 Offline → Online Sync Flow

Sync is not a journey on its own — it is a continuous background concern with explicit user-facing surfaces (the Sync Status Pill, the Sync Center). This section captures the cross-journey behaviour.

Offline windowBehaviourVisible UXNotes
0 min — 1hAll operations against local SQLite continue. Outbox accumulates. AI cloud calls deferred; ONNX edge AI continues.Status pill = green "Working offline" with a pending count.Most common offline window.
1h — 6hSame as above. Local indexes optimized. Reads against snapshot still warm.Status pill = amber "Offline 4h • 23 pending".Encoder uses offline-issuance certificate (J-07).
6h — 24hSame as above. The lock vendor's offline-issuance certificate may expire (default 7 days but configurable; warns at 24h to certificate expiry).Status pill = amber. Banner if certificate near expiry.Key issuance falls back to RFID-only / pre-encoded pool if certificate expires.
> 24hMutations soft-blocked unless the tenant has explicit "Extended offline" enabled. Reads still work.Status pill = red "Sync overdue — contact support".Hard divergence prevention.

Per-aggregate sync semantics (docs/05 Sync Contract; docs/04 §6):

AggregateConflict policy
ReservationServer-authoritative for state transitions; offline holds reconciled with conflict surfacing.
InventoryServer-authoritative; offline allocations are provisional until reconciled.
Folio entriesAppend-only; never last-write-wins; offline entries always replayed in order.
Housekeeping task statusLatest acknowledged completion wins; merge of free-text fields.
Key credentialServer-authoritative; provisional offline credentials reconciled at sync (J-07).
Cash drawer settlementServer-authoritative; offline settlement persisted with pending_sync until reconciled.

Conflict UX. The Sync Center shows each conflict with side-by-side fields, a recommended resolution, and an "Accept recommended / Pick yours / Pick server" affordance. Monetary conflicts are never auto-resolved.

Sync engine internals (operator-visible surfaces). The desktop's sync engine speaks the platform's /sync/v1/pull|push protocol. Operators see four affordances:

  1. Sync Status Pill in the title bar. Colors: green (online, idle), green-pulsing (syncing), amber (offline with pending mutations), red (sync overdue or auth expired). Tooltip shows last successful pull time, last successful push time, pending count.
  2. Sync Center — full-screen view listing pending mutations grouped by aggregate, with per-row retry, snooze (30 min), or "open in detail" affordances.
  3. Conflict drawer — appears as a notification when a conflict is detected during reconciliation. Modal-blocking only for monetary or inventory conflicts; everything else surfaces as a non-blocking alert.
  4. Sync history — last 100 sync cycles with success/failure, duration, and outbox-flush size; useful for diagnosing intermittent connectivity.

Per-aggregate sync ordering is preserved end-to-end: the outbox is a strictly ordered queue per aggregate; cross-aggregate ordering uses the saga sequence numbers from reservation-service. The sync-service server-side accepts a manifest of aggregate IDs + version vectors; the response contains the deltas plus any conflicts the server detected before the desktop attempted to push.

Idempotency is enforced through deterministic Idempotency-Key headers: <deviceId>:<aggregateId>:<localOpId>. The server's idempotency cache (Memorystore) returns the prior response for repeated keys; desktop replays are safe.

7.2 Multi-Language Guest Experience

The platform's locale stack flows through every guest-facing surface:

  • Booking confirmation (J-03/J-04) is rendered in the guest's selected booking-flow language with a secondary-language fallback. Pashto primary + EN secondary; Dari primary + EN secondary; Persian primary + EN secondary.
  • Receipts carry the tenant's settlement currency and the FX-snapshot equivalent in the guest's display currency; numerals follow guest preference.
  • Mobile key invite SMS/WhatsApp text is templated per language; for unsupported scripts on a recipient's device, a Latin-script fallback is sent in parallel.
  • In-stay messaging (J-21) inherits the guest's preferred language from the reservation; staff can override per-message with a translated AI draft (HITL).

Locale switching mid-flow. A guest can switch language at any step. The flow preserves all entered data; messages already sent (e.g., booking confirmation) are not re-sent; future messages adopt the new locale. The desktop arrival board reflects the change so staff can greet the guest correctly.

Calendar systems. Guests in target Phase 0 markets see Solar Hijri presentational dates with Gregorian on hover/tap; Arabic-locale guests see Hijri presentational with Gregorian fallback; receipts always carry both for unambiguous record-keeping. Internally, all dates are stored as ISO-8601 (Gregorian, UTC); calendar conversions are presentational only.

Numerals. Pashto/Dari/Persian users default to Persian-Arabic numerals (۰-۹); Arabic users default to Arabic-Indic numerals (٠-٩); EN/FR users default to European numerals. Tenants can pin a preferred numeral set per surface; receipts and invoices inherit the tenant pin.

7.3 AI HITL Pattern (Generic)

Every AI suggestion in the platform implements the same surface contract:

┌─────────────────────────────────────────────────────────────────────┐
│ AI Suggestion Card │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Suggestion text │ │
│ │ Rationale: <1-line summary> │ │
│ │ Confidence: <bar> │ │
│ │ Provenance: <model>@<version> · prompt:<id> · trace:<id> · ☁/⚙ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ [ Accept ] [ Modify ] [ Reject ] [ Why this? ] │
└─────────────────────────────────────────────────────────────────────┘
  • Accept records reviewedBy + reviewedAt and applies the action.
  • Modify opens an editable view; on save, persists the edited value with provenance preserved.
  • Reject records the reason (free-text optional) and does not apply.
  • Why this? opens a panel with the model, the prompt, and the contributing data points (no model weights).

Irreversible actions (rate change, message send, key issuance, refund) must require Accept/Modify; auto-apply is disallowed unless tenant policy explicitly enables it for a specific surface and the ai-orchestrator-service policy permits it. AI HITL bypass attempts is a tracked KPI with target = 0.

Provenance disclosure. Every AI-driven UI element renders the Provenance Badge ( for cloud, for edge ONNX) inline. The badge is keyboard-accessible and screen-reader announced as "AI suggestion · model X · version Y · click for details". The "Why this?" panel always renders, even if the model is degraded — in which case it explains why the suggestion is unavailable rather than fabricating a rationale.

Per-surface AI catalogue used in the journeys above.

SurfaceUse caseTriggerModel classEdge / CloudMandatory HITL
Booking — name transliteration (J-03)transliterateNameUser taps Latin fieldsmall LM transliterationCloud preferred; manual fallback offlineYes (user edits)
Booking — anomaly detection (J-04)detectAnomaliesAfter payment authtabular ML + LLM rationaleCloudYes (manual review queue)
Check-in — ID OCR (J-05)ocrIdentityDocumentCamera captureDocument-AI / ONNX-OCREdge first, cloud fallbackYes (HITL confirm)
Arrivals — upsell suggestion (J-08)suggestUpsellRow expandLLMCloudYes (clerk approves)
Lock alert — message draft (J-09)draftMessageReach-out CTALLMCloudYes (clerk approves)
Pricing — rate suggestion (J-15)suggestRatesDaily batchforecast + LLM rationaleCloudYes (per suggestion)
Dashboard — anomaly detection (J-16)detectAnomaliesHourly batchmixedCloudYes (alert acknowledgement)
Housekeeping — routing (J-19)suggestAssignmentsTask createdoptimisation + ONNX scorerEdge first, cloud refinementYes (lead approves)

7.4 Operating-Mode Transitions across Journeys

Every journey runs under one of the five operating modes from docs/01 §9. The matrix below summarises which journey behaves how when the mode changes mid-flow:

JourneyFully OnlineDegraded OnlineFully OfflineSync RecoveryAI Degraded
J-01 / J-02FullCached results; bannerPWA cached results; payment blockedn/a (consumer)No personalisation; no AI suggestions; flow unaffected
J-03FullHold + payment retryn/a (consumer flows require online for payment confirm)n/aAI transliteration → manual entry; no blockage
J-04FullCard: degrade to cash-on-arrival promptBlocked at payment stepn/aFraud detection bypassed → enhanced HITL queue
J-05FullID OCR retriesID OCR uses edge ONNX; lock issuance via offline-cert; mobile-key SMS queuedProvisional credentials reconciledManual ID entry only
J-06FullPayment retriesCharges queued; key revoke via offline-certOutbox flush in folio ordern/a
J-07Equivalent to J-05 walk-inSamePrimary mode; fully supportedReservation arbitration; conflict UXManual transliteration
J-08FullSSE retriesReads from SQLite snapshot; mobile-key invites queuedOutbox flush; lock vendor reconciliationNo upsell suggestions
J-09FullVendor retriesLocal fallback (backup card pool)Vendor revoke reconciliationn/a
J-10FullInventory hold retriesWalk-in path (J-07) for groupsMulti-aggregate conflict UXn/a
J-11Fulln/aCredit note local; refund queuedOutbox flushn/a
J-12Fulln/aSettlement local; EOD report from SQLiteOutbox flushn/a
J-13 / J-14Full (online required)Tenant onboarding requires onlinen/an/aTheme palette suggestion → manual
J-15FullSuggestions cachedEdge model produces smoothed forecastn/a"Suggestions unavailable today"
J-16FullStale-projection badgeFirst paint from SQLite; SSE upgrades on reconnectn/aAI insight tile hidden
J-17 / J-18FullGeneration queuedGeneration from SQLite; submission queuedOutbox flush; submission retryn/a
J-19FullAI routing → manualn/a (housekeepers usually share property)n/aManual assignment only
J-20n/an/aPrimary modeConflict UXn/a
J-21FullNotification retriesReads only on guest sideOutbox flushn/a
J-22FullMaintenance creation retriesLocal; reaccommodation deferred until onlineReaccommodation surfaces in Sync Centern/a

7.5 Failure-Mode Common Patterns

FailureWhere it surfacesWhat the user seesWhat the system does
Vendor down (lock, payment, AI)Per-feature affordance + global health badge" currently unavailable — using fallback"Saga retries with backoff; fallback path engaged; alert to GM/Platform
Payment failureBooking flow + checkout"Payment failed — try another method" with issuer-mapped reasonHold remains; user retries; outbox unchanged
Lock issuance failureCheck-in + replace key"Issuing key — please wait" → "Use backup card"Retry with adapter; pre-encoded backup pool; reconciliation at vendor recovery
Sync conflictSync Center"Resolve 1 conflict" pillPer-aggregate merge rule; manual decision for monetary or inventory
AI provider downAny AI affordance"Suggestions unavailable"Hides AI card; non-AI features unaffected; never fabricates suggestions
Notification channel downAny notification pathNo user-visible failure unless explicitBackoff queue; alternate channel; alert if SLO breach
Network drop mid-mutationAny flow with optimistic UIStatus pill flips to amber; "Saved offline" toastOutbox queued; replays at reconnect
Rate-limit (provider or self)Any high-volume action"Slow down — retrying in "Honour Retry-After; exponential backoff
Stale projectionDashboards, reports"Last refresh: " badgeAuto-refresh on focus; re-subscribe to SSE
Fraud anomaly (J-04)Booking flow + GM dashboard"Manual review required — we'll email you"Hold released or held per policy; HITL queue

7.6 Accessibility & RTL across Journeys

Every journey above is verified against the same accessibility and RTL contract:

  • Keyboard reachability. Every primary action (Confirm, Pay, Check-in, Check-out, Mark complete, Approve, Reject) has a single-key or chord shortcut surfaced in the desktop's command palette.
  • Screen-reader announcements. Status changes (booking confirmed, key issued, room ready, sync conflict resolved) are announced through ARIA live regions using ICU-MessageFormat strings localised per surface.
  • Logical CSS only. No left/right margin or padding; no text-align: left; only inline-start/inline-end and text-align: start/end. The shell flips correctly under dir="rtl" for Pashto, Dari, Persian, Arabic.
  • Numerals. Tenant policy chooses Arabic-Indic vs European numerals; receipts and invoices honour the same choice consistently across all journeys.
  • Reduced-motion. Sequence transitions in J-02 (handoff) and the dashboard tile re-renders honour the OS reduced-motion setting.
  • High-contrast mode. All journey screens render correctly under the OS high-contrast setting; tenant-theme tokens are clamped to maintain a 4.5:1 contrast minimum.

7.7 AI Provenance Contract across Journeys

Every AI surface — booking transliteration (J-03), ID OCR (J-05), upsell suggestion (J-08), draft message (J-09 reach-out / J-19 AI routing rationale), pricing suggestion (J-15), anomaly detection (J-16), housekeeping routing (J-19) — attaches a normalised AIProvenance object that is preserved end-to-end through events, audit, and the desktop UI:

{
"model": "vertex-ai/gemini-1.5-flash",
"version": "2026-04-12",
"promptId": "housekeeping/route@1.3.0",
"traceId": "01HQ…",
"reviewedBy": "u_8f1a…", // present after HITL action
"reviewedAt": "2026-04-23T07:14Z", // present after HITL action
"local": false // true if served by ONNX edge model offline
}

This object is the only AI provenance contract; it is shared across ai-orchestrator-service events, downstream consumer events (pricing.dynamic_adjustment.applied.v1, housekeeping.task.assigned.v1), and the audit log. The desktop renders a provenance badge next to every AI artifact; clicking it opens a "Why this?" panel with the prompt template ID, the model version, the inputs, and the evaluation metric the orchestrator used to rank the result.


8. Telemetry across Journeys

The table below is the master mapping of journey events → KPI (01 §10). Frontend events use the frontend.<surface>.<verb> convention; domain events use the canonical Pub/Sub subject from docs/04 §3. Every event carries traceparent, tenantId, propertyId (where applicable), correlationId, and the AI provenance object where applicable.

JourneyFrontend events (selected)Domain events emittedKPI(s) informed
J-01frontend.meta.search_submitted, frontend.meta.results_rendered, frontend.meta.filter_applied, frontend.meta.compare_opened, frontend.meta.property_viewedmelmastoon.search_aggregation.query.submitted.v1, melmastoon.search_aggregation.result_set.served.v1, melmastoon.search_aggregation.filter_applied.v1, melmastoon.search_aggregation.viewport_changed.v1, melmastoon.search_aggregation.property.viewed.v1Meta-layer conversion rate; meta funnel attribution
J-02frontend.tenant.booking_session_started, frontend.tenant.theme_loaded, frontend.tenant.handoff_invalidmelmastoon.bff_consumer.handoff.issued.v1, melmastoon.bff_tenant_booking.session.started.v1Meta→tenant handoff success rate; tenant onboarding completeness
J-03frontend.booking.quote_requested, frontend.booking.method_picked, frontend.booking.confirm_clicked, frontend.booking.confirmedmelmastoon.reservation.quote.created.v1, melmastoon.reservation.held.v1, melmastoon.inventory.allocation.committed.v1, melmastoon.reservation.confirmed.v1, melmastoon.notification.message.queued.v1Direct-booking share; meta-layer conversion; time-to-confirm p95
J-04frontend.booking.three_ds_started, frontend.booking.three_ds_completed, frontend.booking.payment_failedmelmastoon.payment.intent.created.v1, melmastoon.payment.intent.captured.v1, melmastoon.reservation.confirmed.v1Card payment success rate; fraud anomaly rate
J-05frontend.checkin.start_clicked, frontend.checkin.id_scanned, frontend.checkin.id_hitl_overridden, frontend.checkin.key_issued, frontend.checkin.complete_clickedmelmastoon.reservation.check_in_started.v1, melmastoon.ai_orchestrator.suggestion.served.v1, melmastoon.lock_integration.key_credential.issued.v1, melmastoon.reservation.checked_in.v1Time-to-check-in p95; key-issuance success rate; AI HITL bypass = 0
J-06frontend.reservation.modify_date_change, frontend.folio.charge_posted, frontend.checkout.start, frontend.payment.collected, frontend.checkout.completemelmastoon.reservation.dates_changed.v1, melmastoon.lock_integration.key_credential.updated.v1, melmastoon.billing.charge.posted.v1, melmastoon.payment.intent.captured.v1, melmastoon.reservation.checked_out.v1, melmastoon.lock_integration.key_credential.revoked.v1, melmastoon.housekeeping.task.created.v1Avg checkout time; EOD variance
J-07frontend.walkin.created_offline, frontend.walkin.key_encoded_offline, frontend.sync.outbox_flushed, frontend.sync.conflict_surfaced(post-sync) melmastoon.reservation.held.v1, melmastoon.reservation.confirmed.v1, melmastoon.billing.payment.posted.v1, melmastoon.lock_integration.key_credential.issued.v1Sync conflict rate; sync latency p95; offline reliability
J-08frontend.arrivals.eta_set, frontend.arrivals.mobile_keys_batched, frontend.arrivals.late_alertmelmastoon.housekeeping.task.priority_bumped.v1, melmastoon.lock_integration.key_credential.issued.v1, melmastoon.notification.message.queued.v1Time-in-lobby p95; mobile-key delivery latency
J-09frontend.lock.alert_acknowledged, frontend.lock.credential_replaced, frontend.maintenance.work_order_createdmelmastoon.lock_integration.device.health_alert.v1, melmastoon.lock_integration.key_credential.revoked.v1, melmastoon.lock_integration.key_credential.issued.v1, melmastoon.maintenance.work_order.created.v1Lock incident MTTR; preventive task completion
J-10frontend.group.created, frontend.group.deposit_collected, frontend.group.checkout_completemelmastoon.reservation.group_hold.created.v1, melmastoon.inventory.allocation.committed.v1 (×N), melmastoon.billing.folio.opened.v1, melmastoon.billing.payment.posted.v1Group conversion; avg group revenue
J-11frontend.folio.dispute_started, frontend.folio.credit_note_applied, frontend.folio.invoice_reissuedmelmastoon.billing.credit_note.posted.v1, melmastoon.billing.invoice.issued.v1Dispute resolution time; refund cycle time
J-12frontend.eod.close_clicked, frontend.eod.variance_observed, frontend.eod.signed_off_by_twomelmastoon.billing.shift.settled.v1, melmastoon.reporting.report.generated.v1End-of-day variance; % shifts requiring double sign-off
J-13tenant.onboarding.completed, tenant.theme.published, tenant.first_booking_confirmedmelmastoon.tenant.tenant.created.v1, melmastoon.tenant.settings.changed.v1, melmastoon.theme.theme.published.v1, melmastoon.property.property.created.v1Provider-onboarding time
J-14tenant.chain.org_units_created, tenant.chain.cross_property_view_openedmelmastoon.tenant.org_unit.created.v1, melmastoon.property.property.created.v1 (×N)Standardisation compliance score
J-15frontend.ai.suggestion_viewed, frontend.ai.suggestion_actionmelmastoon.ai_orchestrator.suggestion.served.v1, melmastoon.pricing.dynamic_adjustment.applied.v1AI suggestion acceptance rate; AI HITL bypass attempts (= 0)
J-16frontend.dashboard.opened, frontend.dashboard.alert_clicked, frontend.dashboard.action_takenmelmastoon.ai_orchestrator.anomaly.detected.v1, melmastoon.pricing.rate_plan.changed.v1Alert response time
J-17frontend.reports.requested, frontend.reports.downloadedmelmastoon.reporting.report.requested.v1, melmastoon.reporting.report.generated.v1Report generation time
J-18frontend.compliance.daily_report_generated, frontend.compliance.daily_report_submittedmelmastoon.reporting.report.generated.v1, melmastoon.compliance.submission.recorded.v1Compliance submission timeliness
J-19housekeeping.task.created, housekeeping.task.ai_suggested, housekeeping.task.assigned, housekeeping.task.completed, housekeeping.inspection.passed, housekeeping.room.readymelmastoon.housekeeping.task.created.v1, melmastoon.ai_orchestrator.suggestion.served.v1, melmastoon.housekeeping.task.assigned.v1, melmastoon.housekeeping.task.started.v1, melmastoon.housekeeping.task.completed.v1, melmastoon.housekeeping.inspection.passed.v1, melmastoon.housekeeping.room.ready.v1Turnover SLA hit-rate; rooms ready before check-in window
J-20frontend.housekeeping.offline_update, frontend.sync.outbox_flushed(post-sync) melmastoon.housekeeping.task.completed.v1, melmastoon.housekeeping.inspection.passed.v1Sync conflict rate
J-21frontend.guest.service_request_submittedmelmastoon.housekeeping.task.created.v1 (source: "guest")In-stay request response time
J-22housekeeping.maintenance_flagged, maintenance.work_order_created, inventory.room_blockedmelmastoon.housekeeping.room.maintenance_required.v1, melmastoon.maintenance.work_order.created.v1, melmastoon.housekeeping.room.blocked.v1OOS hours avoided; MTTR

9. Cross-References