Skip to main content

J-01 — Discover & Compare on Meta Layer

One-liner: A guest planning a weekend in Kabul finds a guesthouse that matches their constraints and shortlists three candidates before deciding.

1. Purpose

A guest planning a weekend in Kabul wants to find a guesthouse with a halal kitchen, prayer room, and parking, in the AFN 1,500 - 4,000 per-night band, then compare three candidates before deciding. Outcome: the guest holds a shortlist they can act on, and exits this journey to J-02 (handoff into the chosen tenant's booking site).

2. Persona Context

  • Persona: Guest (browser or mobile).
  • Surfaces: Consumer Meta Web (apps/web-meta) and Consumer Mobile (apps/mobile).
  • Primary BFF: bff-consumer-service.
  • Backing services (server side, behind BFF): search-aggregation-service, pricing-service (FX), file-storage-service (signed image URLs), notification-service (rating projection).
  • Preconditions:
    • PWA shell or native app loaded with latest design tokens from @ghasi/ui-melmastoon.
    • search-aggregation-service has indexed published properties (Phase 0: Afghan beachhead tenants).
    • Locale is auto-detected (ps-AF / fa-AF / fa-IR / en-US); currency display defaults to device locale with manual switcher.
  • Trigger: Guest opens https://melmastoon.com (or launches the mobile app) and taps "Find a stay".

3. Entry Points

#EntrySurfaceNotes
1Direct URL https://melmastoon.comWebDefault landing
2Mobile app launchMobileAfter splash + permissions
3Deep link https://melmastoon.com/search?q=kabul&checkIn=...Web/MobileVia universal/app links
4Saved-search push notification (Phase 2)Mobile"New halal stays in Kabul match your filter"
5Marketing campaign UTMWebLands on /search pre-populated

4. Screen-by-Screen Flow

4.1 MetaSearchHero (Home — search input)

  • Layout (text wire): Centered hero with location autocomplete, paired date inputs (check-in / check-out), guest+room steppers, price-band slider, amenity chip multi-select, "Search" CTA.
  • Components: LocationAutocomplete, DateRangePicker (Solar Hijri presentational for ps-AF/fa-*), OccupancyStepper, PriceBandSlider, AmenityChipGroup, Button (variant=emphasis).
  • Offline: Full inputs render from PWA shell; submitting "Search" while offline shows offline banner + queues nothing (search is deferred until reconnect).
  • AI: Optional location autocomplete enhanced by ai-orchestrator-service /suggestLocations (Phase 2); Phase 1 uses local edge top-100-cities cache.
  • Errors: Empty location -> inline error; impossible date range -> stepper auto-corrects.
  • Loading: Inputs always interactive; "Search" CTA spinner if request in flight > 200 ms.
  • A11y: Each input has visible label + aria-describedby for hints; date picker keyboard-navigable per WAI ARIA combobox pattern; chips behave as toggle buttons (aria-pressed).
  • RTL: Logical layout flips; DateRangePicker honours locale calendar direction; numerals follow tenant numerals rule (Latin for displayed date).
  • Perf: LCP element = hero illustration (preloaded); first interaction < 100 ms.
  • Telemetry: view.page (path=/); frontend.meta.search_input_focused (debounced); frontend.meta.search_submitted { criteria }.

4.2 MetaResultsList (default results view)

  • Layout: Top filter bar (sticky on web) + 20 result cards in a virtualised list; each card has hero photo (lazy WebP/AVIF with LQIP), provider name, min/max nightly rate, availability badge, rating, distance from search anchor; a "Compare" toggle in card meta row; "View on map" pill (top-right) toggles to 4.3.
  • Components: FilterBar, ResultCard, LoadingSkeletonList, EmptyState (when 0 results), Pagination.
  • Offline: PWA service worker serves last successful result page from IndexedDB; pill = "Offline - cached results". Filters re-apply locally if subset matches cache.
  • AI: None in Phase 0/1 (privacy: no per-user ranking on meta layer until guest signs in).
  • Errors: MELMASTOON.SEARCH.TIMEOUT -> banner "Slow connection - showing recent results"; MELMASTOON.SEARCH.NO_RESULTS_IN_REGION -> empty state with waitlist email capture.
  • Loading: 8 skeleton cards on first paint; subsequent re-queries fade-swap.
  • A11y: Each card is a single tab stop with aria-label including provider, price range, rating; "Compare" toggle is a separate tab stop with aria-pressed.
  • RTL: Card meta row flips (rating / distance leading vs trailing); price range never re-orders min/max.
  • Perf: Card image lazy-loaded on viewport intersection; FlashList on mobile; window virtualisation on web.
  • Telemetry: frontend.meta.results_rendered { count, latency_ms }; frontend.meta.card_clicked { propertyId, position }.

4.3 MetaResultsMap (map view)

  • Layout: Full-viewport map with clustered pins; bottom sheet (mobile) / right rail (web) showing the currently-tapped property's quick-info; toggle back to list.
  • Components: MapCanvas (Leaflet web; react-native-maps mobile), MarkerCluster, PinInfoSheet, MapToggle.
  • Offline: Map tiles cached for last viewport; new pan/zoom shows cached pins only; "Offline - panning disabled" affordance.
  • AI: None.
  • Errors: Tile load failure -> grey-out tile with retry CTA.
  • Loading: Pins faded in over 200 ms after viewport debounce.
  • A11y: Map has fallback list link for screen-reader users; pins are not interactive via screen reader; the "View list" affordance gives keyboard-accessible parity.
  • RTL: Map content does not mirror; chrome (sheet, toggles) flips.
  • Perf: Bounding-box re-query debounced 350 ms; max 200 pins rendered at once before clustering kicks in.
  • Telemetry: frontend.meta.viewport_changed { boundsHash } (debounced); frontend.meta.pin_tapped { propertyId }.

4.4 MetaCompareDrawer (comparison view)

  • Layout: Side-by-side table for 2-3 properties; columns are properties, rows are attributes (price band, amenities checklist, rating, cancellation policy, distance, halal-certified flag, prayer-room availability).
  • Components: CompareTable, CompareCard (sticky property header), Button (primary "Continue with this hotel").
  • Offline: Comparison is computed locally from cached property summaries; CTA disabled with explanation.
  • AI: None in P1; P2 introduces "Best fit for you" highlight with provenance.
  • Errors: Missing comparison data field -> "n/a" cell, never blank.
  • Loading: Skeleton table while fetching GET /api/v1/search/compare?ids=....
  • A11y: Table uses <th scope="col"> for property headers, <th scope="row"> for attribute rows; each "Continue" button has full property name in aria-label.
  • RTL: Column order reverses for RTL locales; the "selected" property visual cue follows reading direction.
  • Perf: Comparison opens in <= 300 ms; table render <= 100 ms.
  • Telemetry: frontend.meta.compare_opened { ids }; frontend.meta.compare_continue_clicked { propertyId }.

4.5 PropertyDetailScreen

  • Layout: Hero gallery (5+ photos, thumbnails), title block, amenities grid, policies accordion, room-type preview cards, reviews block (lazy), embedded map, sticky "Book on hotel site" CTA bottom.
  • Components: Gallery, AmenitiesGrid, PolicyAccordion, RoomTypeCard, ReviewBlock, MapEmbed, Button (sticky CTA).
  • Offline: Last 20 viewed property detail responses served from IndexedDB / MMKV; sticky CTA shows offline state and disables.
  • AI: None on consumer surface in P1; P2 may insert AI-generated summary chips.
  • Errors: Image load failure -> skeleton placeholder, never broken-image icon; description missing -> hide row.
  • Loading: Skeleton matches final layout; gallery thumbnails first, hero second.
  • A11y: Heading levels respected (h1 = property name); gallery is keyboard-navigable carousel; reviews use aria-live="polite" for "load more" announcements.
  • RTL: Gallery scroll direction follows reading direction; price layout does not flip.
  • Perf: LCP target <= 2.5 s on Slow 4G / Moto G4; first network call yields hero photo srcset; below-the-fold lazy.
  • Telemetry: frontend.meta.property_viewed { id }; frontend.meta.gallery_image_viewed { index }; frontend.meta.cta_book_clicked { propertyId, tenantId } (transitions to J-02).

5. State Machine

6. Data Requirements

6.1 Server state (TanStack Query / RN Query)

QueryKeyBFF endpointstaleTimegcTimeNotes
searchProperties['consumer.v1', 'search', sanitizedQuery]POST /api/v1/search/properties30 s5 minEdge-cached 15 s; query-keyed
searchMap['consumer.v1', 'search', sanitizedQuery, 'map']POST /api/v1/search/map30 s5 minBounding-box variant
getCompare['consumer.v1', 'compare', ids]GET /api/v1/search/compare?ids=...60 s5 minReturns denormalised projection
getProperty['consumer.v1', 'hotel', propertyId]GET /api/v1/properties/:id60 s30 minAggregates property + pricing + ratings
getFacets['consumer.v1', 'facets', country]GET /api/v1/facets?country=...5 min30 minUsed by location autocomplete fallback

6.2 URL state

  • /search?q=&checkIn=&checkOut=&adults=&rooms=&priceMin=&priceMax=&amenities=&sort=&view=list|map
  • Filters and sort are deep-linkable (SEO + share). View toggle persists in URL.

6.3 Local persistence

  • IndexedDB (web) / MMKV (mobile): last 50 search responses; last 20 property details; locale + currency preference; recent searches.
  • No PII stored client-side.

6.4 Idempotency

  • All requests are GET / safe POST (search). No mutations on this journey.

7. AI Behavior

n/a in Phase 0/1 — privacy stance: no per-user personalisation on meta layer until guest signs in. Phase 2 introduces (with explicit consent and provenance):

  • ai-orchestrator-service /suggestLocations for autocomplete enhancement
  • ai-orchestrator-service /rerank for personalised ranking
  • "Best fit for you" callout in compare drawer

When introduced, all AI elements MUST follow the canonical HITL pattern (no auto-action; provenance shown).

8. Offline Behavior

  • PWA service worker serves last successful results page from IndexedDB (web).
  • Mobile RN reads from MMKV for last 50 search responses.
  • MetaResultsList shows "Offline - cached results" pill.
  • New search: queued on web (deferred until reconnect); on mobile, search button disabled with banner.
  • PropertyDetailScreen: 20 cached property details readable; "Book on hotel site" CTA disabled with offline banner.
  • Map view: cached pins only; pan/zoom outside cache greys tiles with retry.

9. Error States

ErrorTriggerUX shownRecoveryTelemetry
MELMASTOON.SEARCH.TIMEOUTsearch-aggregation-service p99 breachedBanner: "Slow connection - showing recent results" + cached set if anyAuto-retry on next search submit; user can manually retryerror.surfaced { code, traceId }
MELMASTOON.SEARCH.NO_RESULTS_IN_REGIONPhase 0 limited geo coverageEmpty state: "We're not in this city yet - tell us where to launch next" + waitlist emailUser edits criteria or submits waitlistfrontend.meta.no_results { country }
MELMASTOON.PRICING.FX_STALEFX feed stale > 24 h for target currencyDisplay falls back to USD with tooltipTooltip explains snapshot freshness; user can pick another currencyerror.surfaced { code }
MELMASTOON.STORAGE.MEDIA_LOAD_FAILEDCDN image fetch failureSkeleton placeholder remains; provider name + rate visibleAuto-retry on next viewport intersectionfrontend.meta.image_failed { propertyId }
MELMASTOON.SEARCH.RATE_LIMITEDclient-side rate-limit (rare)Toast "Too many searches - please wait s"Honour Retry-Aftererror.surfaced { code }

10. E2E Test Gates

GateScenarioSurface
G-WEB-1 (composite, see ../../common/10-frontend-testing-strategy.md §3)Search -> filter -> map toggle -> property detail -> handoffWeb meta + tenant booking
G-MOB-1 (composite)Discover (search) -> property detail -> "Book" -> bootstrap tenant -> ...Mobile
Per-screen testsLCP <= 2.5 s on results screen on Slow 4G; compare drawer opens <= 300 ms; RTL render verified for PashtoWeb + Mobile

11. Performance Requirements

MetricTargetSource
LCP (results screen, p75)< 2.5 s on Slow 4G / Moto G4NFRs §1.1
INP (filter / sort interaction)< 200 msNFRs §1.1
Compare drawer open<= 300 msthis journey
Property detail mount<= 1.5 s p95 (mobile cold)NFRs §1.2
First network call payload<= 80 KiB gzippedNFRs §7 capacity

12. Accessibility Requirements

  • WCAG 2.2 AA across all five screens.
  • Each card on the results list is a single tab stop with comprehensive aria-label.
  • Date picker is keyboard-operable per WAI ARIA combobox pattern.
  • Map fallback: list link is always keyboard-reachable.
  • Hero gallery is a keyboard-navigable carousel (left/right arrows; Home/End to first/last; Esc to close fullscreen).
  • Touch targets >= 44 x 44 pt mobile / 32 x 32 px web.
  • Reduced-motion: gallery transitions collapse to opacity fades; map pin animations disabled.
  • Screen reader announces results count change ("12 results found").

13. Telemetry

Frontend events

EventWhenProperties
view.page / view.screenMount of any screen in this journeypath, tenantId?, propertyId?
frontend.meta.search_submittedSubmit searchcriteria (sanitised)
frontend.meta.results_renderedResults list paintscount, latency_ms
frontend.meta.filter_appliedToggle a filterfilter, value
frontend.meta.viewport_changedMap pan/zoom (debounced)boundsHash
frontend.meta.compare_openedCompare drawer opensids
frontend.meta.property_viewedPropertyDetail mountid
frontend.meta.cta_book_clickedTap "Book on hotel site"propertyId, tenantId
error.surfacedVisible errorerrorCode, httpStatus, path, traceId

Domain events emitted (server side)

  • melmastoon.search_aggregation.query.submitted.v1 (sampled)
  • melmastoon.search_aggregation.result_set.served.v1
  • melmastoon.search_aggregation.filter_applied.v1
  • melmastoon.search_aggregation.viewport_changed.v1 (debounced)
  • melmastoon.search_aggregation.property.viewed.v1

14. Success Criteria

  • LCP <= 2.5 s on Slow 4G for results screen (Moto G4 emulation).
  • All copy renders in user's locale (Pashto / Dari / Persian / EN); RTL layout correct end-to-end.
  • Min/max prices reconcile with pricing-service snapshot within +/- 0.5%.
  • Compare drawer opens <= 300 ms.
  • Property detail mounts <= 1.5 s p95 on mobile cold.
  • Zero broken-image icons on result cards (skeleton fallback always).
  • E2E gate G-WEB-1 step "search to property detail to handoff" passes on every release branch.

References