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 — 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)

  • InventoryTemplateGroupbelongs_to :restaurant, `has_many
    inventory_templates. Referenced by Restaurant.mon..sun(+*_take_away`) integer FKs = the day-of-week schedule.
  • InventoryTemplatebelongs_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_datereturn if inventory_template_groups.blank?; per group → find_inventory_templates_for_dateday = 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? AND
  • restaurant.<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 existing inventory_calendar / inventory_packages pattern) → renders a _slots_body partial 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

  1. A date whose weekday has an InventoryTemplateGroup with 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.
  2. restaurant.<weekday> unset → calm “Closed on {weekday}s” state (grey, not red config-mistake).
  3. Weekday group set but empty → red “group has no time templates”.
  4. No template groups at all → red “no opening hours configured”.
  5. Predicate uses the channel’s service_type (dine_in vs take_away), not hardcoded dine_in.
  6. Per-slot breakdown shows override badge (override qty + template qty + reason; graceful degrade) for overridden slots; no verdict change.
  7. The per-slot breakdown is AJAX-deferred; package-detail initial load is shell-only.
  8. 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 UpdatedInventory semantics — 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_present reasons + 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, Restaurant weekday columns
  • Overrides + AJAX slots: bookability_presenter.rb#slot_breakdown, new inventory_slots action + _slots_body.html.erb, _restaurant_package_level.html.erb (skeleton + fetch)