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 — “Outside Booking Window” Calendar State (days_in_advance + expiry)

Status: Drafted from grilling; ready for review Branch: continues perf/hhserver-vendor-availability-major Tracking: EPIC: Outside booking window calendar state #8330 - tracked on PR #8274 Audience: HungryHub Engineering + CS-Ops Relationship to prior work: Extends the shipped bookability diagnosis — adds a calm “outside booking window” date state, alongside the existing
awaiting_generation / cache / config states.

1. Problem

On the admin inventories diagnosis (observed on staging restaurant 4391, hh-ballbot), many days show as not-bookable red — but for a large share of them the real reason is simply that the date is beyond the restaurant’s booking window (days_in_advance), which is by design, not a problem.

The calendar currently makes only two boundaries explicit — past date and after expiry_date — and even expiry_date renders as alarming red. A date beyond today + days_in_advance has no dedicated handling, so it falls through to a generic red “No availability / not bookable,” indistinguishable from a genuine misconfiguration. Ops can’t tell “outside the window (expected)” from “something is broken.”

2. Grounded reality (both sides use days_in_advance identically)

  • Inventory creation stops at the window: AutoExtendInventorySubWorker generates dates only up to today + days_in_advance (auto_extend_inventory_sub_worker.rb:163); supplier sync four_days syncs up to days_in_advance (scheduler_service.rb:186).
  • Booking rejects beyond the window: the inv_checker returns inventory.greater_than_day_in_advance for date > (today + days_in_advance) (inv_checker_hungry_hub_service.rb:1864, strict >).
  • ⇒ beyond-window dates are not-bookable BY DESIGN — no inventory exists AND booking would reject them. Nothing is wrong.
  • The window is already known to the diagnosis: restaurant_verdict prints “Booking window: today → {min(today+days_in_advance, expiry_date)}”. It’s just not reflected per-day in the calendar.
  • Latent inconsistency: the expiry_date calendar branch renders :not_bookable (red) with a reason string — not a visually distinct state. This PRD fixes that too by giving both boundaries the same calm state.

3. Goal

Make the calendar (and date-detail path) show days beyond the booking window in a distinct, calm grey “outside booking window (expected)” state — clearly separate from red “this is a real problem” days — so “many days not bookable” reads as expected boundary, not misconfiguration.

4. Solution

4.1 One shared window verdict (date-level, short-circuits the cascade)

Add date_window_verdict(date) on the presenter. It is evaluated first, before any per-date/per-package computation, by BOTH the calendar (date_verdicts) and the date-detail path (restaurant-level view / attribution):

  • Past: date < today → keep the existing “Date in the past”.
  • Beyond days_in_advance: days_in_advance.present? && date > (today + days_in_advance) (strict >, so today + days_in_advance is the LAST bookable day — matches the booking checker exactly).
  • After expiry: expiry_date.present? && date > expiry_date.
  • If beyond either window boundary → return an :outside_window verdict and short-circuit — skip calc_find_available_date and package diagnoses entirely (correctness AND a perf win: out-of-window days do no heavy work).
  • Otherwise return nil → the normal cascade runs.

4.2 Reason text — show BOTH boundaries when both apply

  • Only days_in_advance crossed → “Outside booking window — beyond the {days_in_advance}-day advance limit (bookable until {today+days_in_advance}).”
  • Only expiry crossed → “Outside booking window — after restaurant expiry ({expiry_date}).”
  • Both crossed → name both: “Outside booking window — beyond the {N}-day advance limit (until {date1}) and after restaurant expiry ({date2}).”

4.3 New calm visual state

  • New verdict status :outside_window → CSS bk-day--outside-window (grey, reusing the existing muted/hidden palette — NOT red).
  • The date-detail headline verdict uses the same calm state + reason (so clicking a grey calendar day lands on a consistent, non-alarming detail page).
  • Legend gains an entry: “⬜ Outside booking window”.
  • The header fact “Booking window: today → X” stays — so the boundary is stated three ways: header text, the calendar’s visible green→grey edge, and per-day tooltip.

4.4 Edit affordance (reuse, don’t build)

The reason links to editing days_in_advance on the restaurant via the existing restaurant_days_in_advance gating field’s edit target (edit: :restaurant). No new action is built.

5. Acceptance criteria

  1. A date > today + days_in_advance renders as a grey :outside_window tile (not red), with a reason naming the advance limit and the last bookable date.
  2. today + days_in_advance itself (the boundary day) is NOT outside-window — it renders via the normal cascade (green if it has availability). Strict >.
  3. A date > expiry_date renders in the SAME grey state (fixing the prior red inconsistency), reason “after restaurant expiry”.
  4. A date beyond BOTH boundaries names both in the reason.
  5. date_window_verdict short-circuits: out-of-window dates do NOT call calc_find_available_date or package_diagnoses (verified — perf + no misleading per-package config reasons).
  6. The calendar shows the green→grey transition at the window edge; the legend explains the grey state.
  7. The date-detail page for an out-of-window date shows the same calm state, not a red config/not-bookable verdict.
  8. No regression: in-window dates diagnose exactly as before (parity green).

6. Non-goals (v1 — surface only)

  • No change to days_in_advance semantics or the booking-rejection rule (inv_checker:1864).
  • No change to inventory creation (auto-extend / supplier sync horizons) — they correctly stop at the window.
  • No new “extend the window” action — ops edit days_in_advance via the existing restaurant edit page (we only link to it).
  • No customer-facing changes.

7. Risks

  • Boundary off-by-one: must use strict > to match inv_checker:1864 exactly, or the calendar and the real booking rule disagree on the last bookable day. Covered by acceptance #2 + spec.
  • Timezone: today must be computed in the restaurant time_zone (the presenter already does this via Time.now_in_tz).
  • days_in_advance nil/zero: guard — if unset, no advance boundary applies (only expiry, if present).

8. Source-of-truth file map

  • Presenter: app/presenters/admin/bookability_presenter.rb (date_verdicts, the expiry branch to unify, restaurant_verdict “Booking window” fact, last_bookable_date)
  • Booking rule to mirror: inv_checker_hungry_hub_service.rb:1864
  • Gating field (edit link): bookability/gating_fields.rb :restaurant_days_in_advance
  • Views: _calendar_body.html.erb, _overview.html.erb (legend), _restaurant_level.html.erb (date-detail headline), _bookability_styles.html.erb