PRD — Fix missing-inventory predicate (template groups, not UpdatedInventory) + surface overrides
Status: Drafted from grilling; ready for build
Branch: continues perf/hhserver-vendor-availability-major
Tracking: EPIC: Fix missing-inventory predicate + surface overrides #8339 - tracked on PR #8274
Audience: HungryHub Engineering + CS-Ops
Relationship to prior work: Corrects a WRONG reason shipped in SUB2
(#8320, docs/PRD-missing-inventory-autoextend-diagnosis.md) and adds an
override indicator.
1. Problem
On /admin/restaurants/1631/inventories (package 4338, 2026-08-03) the
diagnosis says:
“No inventory for this date, and it will NOT auto-generate — no inventory template (UpdatedInventory) covers this date.”
This is wrong. UpdatedInventory is not the generation source. The nightly
AutoExtendInventorySubWorker generates Inventory rows from
InventoryTemplate rows, selected by the day-of-week
InventoryTemplateGroup (Restaurant.mon..sun / mon_take_away..).
UpdatedInventory is only a per-slot quantity override applied on top.
SUB2’s FieldContext#updated_inventory_covers_date? queries the wrong model
(and hardcodes service_type: 'dine_in'), so restaurants that generate via the
normal day-of-week template groups get a false “no template covers this date.”
2. The four models (confirmed)
-
InventoryTemplateGroup—belongs_to :restaurant, `has_many- inventory_templates
. Referenced byRestaurant.mon..sun(+*_take_away`) integer FKs = the day-of-week schedule.
InventoryTemplate—belongs_to :inventory_template_group;start_time/end_time/quantity_available. The generation source.Inventory— the per-date rows generated nightly from templates.UpdatedInventory— per-slot override:start_date..end_date,start_time..end_time,quantity_available,reason,service_type. Applied on top of a matching template’s quantity; not the generation gate.
Worker logic (auto_extend_inventory_sub_worker.rb):
generate_inventory_for_date → return if inventory_template_groups.blank?;
per group → find_inventory_templates_for_date → day = DAY_MAPPING[date.wday];
template_ref = restaurant.send(day) (dine_in) / send("#{day}_take_away")
(take_away); if template_ref == itg.id → use itg.inventory_templates.
3. Fix A — correct “will this date auto-generate?” predicate
Replace updated_inventory_covers_date? with a weekday-aware
templates_cover_date?(date, service_type) that mirrors the worker:
restaurant.inventory_template_groups.present?ANDrestaurant.<weekday>(dine_in) /<weekday>_take_away(take_away) is a present group id AND- that group has
inventory_templates.
service_type is derived from the diagnosis channel (dine_in / take_away),
not hardcoded. The predicate is window-agnostic — #8331’s
date_window_verdict already short-circuits beyond-window dates before this runs.
Resulting states (missing inventory, in scope, in window)
- ✅ Self-healing — weekday group has templates, inventory just not generated yet. (unchanged meaning; now correctly detected)
- ⬜ Closed this weekday (NEW, calm/low-alarm — grey, not red) —
restaurant.<weekday>is unset (nil/0): the restaurant has no opening-hours group for this weekday, so the job will never generate it. Reason: “Closed on {weekday}s — no opening-hours group is assigned for this day. If it should be open, assign a template group for {weekday}.” + edit-restaurant link. - 🔴 Group empty (real problem) — weekday group is set but has NO
inventory_templates. Reason: “Opening-hours group #{id} for {weekday} has no time templates — add opening hours.” - 🔴 No template groups at all (real problem) — restaurant has zero
inventory_template_groups. Reason: “No opening hours configured for this restaurant.” - 🔴 Out of scope — (unchanged) expired / booking off.
4. Fix B — surface UpdatedInventory overrides (slot level, informational)
On the package-detail per-slot breakdown, for each slot covered by an
UpdatedInventory (matching start_date..end_date ⊇ date and
start_time..end_time ⊇ the slot):
- Badge: “⚙ override: {override_qty} (template {template_qty}) — {reason}”.
- The template qty comes from the covering
InventoryTemplate; if it isn’t cheaply resolvable for a slot, gracefully degrade to “⚙ override applied — {reason}” rather than omitting the badge. - Informational only — does NOT change any verdict. It explains why a slot’s quantity differs from the template default.
5. Fix C — AJAX-defer the per-slot breakdown
The package-detail per-slot table currently renders inline (slot_breakdown
computed in _restaurant_package_level.html.erb), which is a latent timeout
risk AND where the new override lookups would land. Defer it:
- New fragment endpoint
GET .../inventory_slots(mirrors the existinginventory_calendar/inventory_packagespattern) → renders a_slots_bodypartial containing the slot table (now with override data). - The package-detail page renders a skeleton + fetch (like the overview calendar); the slot compute + override lookups run in the deferred request.
- Initial page load stays shell-only.
6. Acceptance criteria
- A date whose weekday has an
InventoryTemplateGroupwith templates is NO longer labeled “no UpdatedInventory template” — it is self-healing (if no inventory yet) or bookable (if generated). The 1631/4338 case is fixed. restaurant.<weekday>unset → calm “Closed on {weekday}s” state (grey, not red config-mistake).- Weekday group set but empty → red “group has no time templates”.
- No template groups at all → red “no opening hours configured”.
- Predicate uses the channel’s service_type (dine_in vs take_away), not hardcoded dine_in.
- Per-slot breakdown shows override badge (override qty + template qty + reason; graceful degrade) for overridden slots; no verdict change.
- The per-slot breakdown is AJAX-deferred; package-detail initial load is shell-only.
- No regression: parity green; the three existing states still classify correctly; #8331 outside-window still short-circuits first.
7. Non-goals
- No change to the worker or generation logic — diagnosis only.
- No change to
UpdatedInventorysemantics — we only read/display it. - No calendar-level override display (slot level only; a date-level “has overrides” hint is optional, not required).
- No customer-facing changes.
8. Source-of-truth file map
- Fix predicate:
app/presenters/admin/bookability/field_context.rb(updated_inventory_covers_date?→templates_cover_date?+ weekday/service helpers),bookability/gating_fields.rb(inventory_presentreasons + severity),bookability_presenter.rb(state wiring) - Worker mirrored:
auto_extend_inventory_sub_worker.rb(find_inventory_templates_for_date,decide_inventory_template_ref,DAY_MAPPING) - Models:
inventory_template_group.rb,inventory_template.rb,updated_inventory.rb,Restaurantweekday columns - Overrides + AJAX slots:
bookability_presenter.rb#slot_breakdown, newinventory_slotsaction +_slots_body.html.erb,_restaurant_package_level.html.erb(skeleton + fetch)