Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PRD — Third-Party Supplier Inventory Time-Window Mismatch Diagnosis

Status: Drafted from grilling; ready for review Branch: continues perf/hhserver-vendor-availability-major Tracking: EPIC: Third-party supplier inventory mismatch diagnosis #8322 - tracked on PR #8274 Audience: HungryHub Engineering + CS-Ops Relationship to prior work: Extends the bookability diagnosis (docs/PRD-bookability-diagnosis.md) — adds a third-party attribution branch, analogous to the cache-vs-config and missing-inventory work already shipped.


1. Problem (real incident)

“Quan’s Kitchen at Four Points by Sheraton KL” uses SevenRooms as its inventory source. A few packages weren’t bookable. Root cause took engineers hours to find: SevenRooms was serving inventory for 07:00–16:00, but the packages are set for dinner time slots (e.g. 18:00–21:00) — inventory existed, but at times that don’t overlap the package’s agenda. The underlying cause was a SevenRooms-side setting (access-rules / audience), plus “HungryHub didn’t sync correctly.” A teammate fixed it with a force update, noting: once the restaurant fixes their SevenRooms settings, HungryHub must also force-update inventory for all advance days.

The current bookability diagnosis would only say “sold out / no slot” — it never surfaces “there IS supplier inventory, just at the wrong times.” That missing insight is the entire cost of the incident.

2. Goal

For a third-party (non-HungryHub) restaurant, when a package isn’t bookable, tell CS-ops which side is the problem and let them act:

  • If supplier inventory exists but its time window doesn’t overlap the package agenda → show the mismatch explicitly + a copy-pasteable advisory to relay to the restaurant, and offer a force-update (all advance days) action.
  • Ops decides: force-update; if that doesn’t fix it, it’s a supplier-side setting the restaurant must change.

Turn a multi-hour engineer debug into a 2-minute self-service CS action.

3. Grounded reality (what the codebase already gives us)

  • Inventory source: Restaurant.inventory_source (inv_source: hungryhub / sevenrooms / tablecheck / bistrochat / mymenu). restaurant.use_third_party_inventory? is true for non-HH sources.
  • Supplier inventory times ARE readable: Inventory::InvCheckerSupplierService (< InvCheckerHungryHubService) has its own #inventories(date, slug) that queries the supplier inventory model and returns start_time_format (e.g. “07:00”..“16:00”) — the SAME interface FieldContext already uses.
  • Package agenda times: rp.available_times(date, tz) (already read by FieldContext#open_times) returns the package’s required slots (e.g. 18:00–21:00).
  • ⇒ The mismatch is a set comparison we can compute directly: supplier_inventory_times ∩ package_agenda_times = ∅.
  • Force-update already exists: force_update_supplier_inventory enqueues Suppliers::RestaurantsInventorySyncSchedulerWorker(supplier, [id], trigger_type) with trigger_type = four_days / one_day / four_hours (bounded). Suppliers::RestaurantInventorySyncWorker(supplier, id, start_date, end_date) syncs an arbitrary date range — the primitive for “all advance days”.

4. Primary user & surface

  • Primary: CS-ops on the admin inventories diagnosis page.
  • v1 surface: the existing diagnosis page (restaurant-level / package-level).
  • Conditional: only rendered when restaurant.use_third_party_inventory?.

5. Solution

5.1 Time-window mismatch detection

For a third-party restaurant, when a package is not bookable AND its config gating fields pass (active/dates/agenda OK), compute:

  • supplier_times = start times from the supplier inventory for the date (InvCheckerSupplierService#inventories(date).map { 'start_time_format' }).
  • agenda_times = rp.available_times(date, tz).
  • Mismatch = supplier_times.present? && (supplier_times ∩ agenda_times).empty? (inventory exists, but none of it overlaps the package’s needed slots).

This becomes a new attribution outcome distinct from “sold out” and “no inventory at all”.

5.2 Specific advisory (generic supplier terminology)

When a mismatch is detected, render a filled-in, copy-pasteable advisory:

⚠ Supplier inventory is at the wrong times. {Supplier} is serving inventory for {supplier_window} (e.g. 07:00–16:00), but package “{package}” needs {agenda_window} (e.g. 18:00–21:00) — they don’t overlap.

1. Ask the restaurant to check their {Supplier} access-rules / audience settings so it serves the needed times. 2. Once they’ve fixed it, force-update all advance days here.

Times/package/supplier are filled in specifically; the “access-rules/audience” phrasing is generic (not per-supplier — deferred). {supplier_window} is a compact representation of the supplier times present (min–max, or a short list).

5.3 Force-update actions

On the diagnosis page for third-party restaurants:

  • Existing: “Force Update Supplier Inventory” (4-day quick sync) — keep.
  • New: “Force-update supplier — all advance days” → syncs the full [today, today + restaurant.days_in_advance] window via the supplier sync (range-based RestaurantInventorySyncWorker or the scheduler with a full-range param). This encodes the teammate’s “refresh inventories for all advance days”. Admin-namespaced, POST, CSRF, single-restaurant scoped.
  • (Optional live status: may reuse the WarmStatus banner pattern; not required for v1 acceptance.)

5.4 Attribution ordering (extends the existing diagnosis)

For a third-party package that is not bookable:

  1. Config gating fails (active/visible/dates/agenda) → existing diagnosis.
  2. Config OK, supplier inventory exists but mismatched times → §5.2 advisory
    • §5.3 actions. This is the new branch.
  3. Config OK, agenda overlaps, but no supplier inventory at all → “supplier returned no inventory — force-update; if still empty, it’s a supplier setting.”
  4. Otherwise existing sold-out/seat/duration reasons.

6. Acceptance criteria

  1. For a third-party restaurant + package whose agenda doesn’t overlap the supplier inventory times, the diagnosis shows the mismatch advisory with the actual supplier window, the package’s needed window, and the two-step next action — NOT a bare “sold out / no slot”.
  2. The advisory names the supplier (inv_source) and the specific package.
  3. A “Force-update all advance days” action exists for third-party restaurants, enqueues the supplier sync across [today, today+days_in_advance], and is POST + CSRF + single-restaurant scoped.
  4. The existing 4-day force-update button still works.
  5. The mismatch branch is only evaluated for use_third_party_inventory? restaurants (no behavior change for HungryHub-source).
  6. No regression to existing bookability / auto-extend / cache-warming diagnosis.
  7. Read-only diagnosis path triggers no sync; only the explicit force-update POST enqueues work.

7. Non-goals (v1)

  • Sync-freshness / last-run tracking for supplier sync (the discriminator between “supplier serving wrong times” vs “our sync stale”) — deferred. v1 shows the mismatch and lets ops decide via force-update.
  • Per-supplier terminology (SevenRooms “access-rules/audience” vs others) — generic phrasing in v1.
  • No changes to the supplier sync workers themselves (only a new range trigger).
  • No auto-contacting the restaurant — we only produce the message for ops.
  • Nothing for HungryHub-source restaurants (already covered).
  • No HungryHub-native “all advance days” for third-party (the auto-extend Generate-now from #8318 is for HH-source; supplier restaurants use the supplier sync range action here).

8. Risks

  • Supplier inventories(date) cost: it’s a direct SQL read per date; the mismatch check runs on the diagnosis page only (bounded), reuse the same memoization as FieldContext.
  • False “mismatch”: if available_times is itself empty (agenda closed that day) the comparison must not scream “mismatch” — guard: only a mismatch when BOTH supplier times AND agenda times are present but disjoint.
  • Full-advance-window sync load: syncing days_in_advance (can be large) hits the supplier API harder than the 4-day button — reuse existing rate-limit handling in RestaurantInventorySyncWorker; consider chunking.
  • Timezone alignment: supplier start_time_format and available_times must be compared in the same tz (restaurant time_zone) — both already are.

9. Source-of-truth file map

  • Supplier checker: app/services/inventory/inv_checker_supplier_service.rb (#inventories, #inventory_model_name)
  • Agenda times: app/models/hh_package/restaurant_package.rb#available_times
  • Existing force-update: app/controllers/admin/restaurants_controller.rb#force_update_supplier_inventory
    • Suppliers::RestaurantsInventorySyncSchedulerWorker / Suppliers::RestaurantInventorySyncWorker
  • Diagnosis to extend: app/presenters/admin/bookability_presenter.rb, app/presenters/admin/bookability/field_context.rb, the inventories views
  • Source flags: restaurant.use_third_party_inventory?, inventory_source.inv_source