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 returnsstart_time_format(e.g. “07:00”..“16:00”) — the SAME interfaceFieldContextalready uses. - Package agenda times:
rp.available_times(date, tz)(already read byFieldContext#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_inventoryenqueuesSuppliers::RestaurantsInventorySyncSchedulerWorker(supplier, [id], trigger_type)withtrigger_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-basedRestaurantInventorySyncWorkeror 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:
- Config gating fails (active/visible/dates/agenda) → existing diagnosis.
- Config OK, supplier inventory exists but mismatched times → §5.2 advisory
- §5.3 actions. This is the new branch.
- Config OK, agenda overlaps, but no supplier inventory at all → “supplier returned no inventory — force-update; if still empty, it’s a supplier setting.”
- Otherwise existing sold-out/seat/duration reasons.
6. Acceptance criteria
- 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”.
- The advisory names the supplier (
inv_source) and the specific package. - 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. - The existing 4-day force-update button still works.
- The mismatch branch is only evaluated for
use_third_party_inventory?restaurants (no behavior change for HungryHub-source). - No regression to existing bookability / auto-extend / cache-warming diagnosis.
- 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_timesis 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 inRestaurantInventorySyncWorker; consider chunking. - Timezone alignment: supplier
start_time_formatandavailable_timesmust 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_inventorySuppliers::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