PRD — “Outside Booking Window” Calendar State (days_in_advance + expiry)
- Status: Drafted from grilling; ready for review
Branch: continues
perf/hhserver-vendor-availability-majorTracking: 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:
AutoExtendInventorySubWorkergenerates dates only up totoday + days_in_advance(auto_extend_inventory_sub_worker.rb:163); supplier syncfour_dayssyncs up todays_in_advance(scheduler_service.rb:186). - Booking rejects beyond the window: the inv_checker returns
inventory.greater_than_day_in_advancefordate > (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_verdictprints “Booking window: today → {min(today+days_in_advance, expiry_date)}”. It’s just not reflected per-day in the calendar. - Latent inconsistency: the
expiry_datecalendar 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>, sotoday + days_in_advanceis 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_windowverdict and short-circuit — skipcalc_find_available_dateand 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→ CSSbk-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
- A date
> today + days_in_advancerenders as a grey:outside_windowtile (not red), with a reason naming the advance limit and the last bookable date. today + days_in_advanceitself (the boundary day) is NOT outside-window — it renders via the normal cascade (green if it has availability). Strict>.- A date
> expiry_daterenders in the SAME grey state (fixing the prior red inconsistency), reason “after restaurant expiry”. - A date beyond BOTH boundaries names both in the reason.
date_window_verdictshort-circuits: out-of-window dates do NOT callcalc_find_available_dateorpackage_diagnoses(verified — perf + no misleading per-package config reasons).- The calendar shows the green→grey transition at the window edge; the legend explains the grey state.
- The date-detail page for an out-of-window date shows the same calm state, not a red config/not-bookable verdict.
- No regression: in-window dates diagnose exactly as before (parity green).
6. Non-goals (v1 — surface only)
- No change to
days_in_advancesemantics 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_advancevia the existing restaurant edit page (we only link to it). - No customer-facing changes.
7. Risks
- Boundary off-by-one: must use strict
>to matchinv_checker:1864exactly, or the calendar and the real booking rule disagree on the last bookable day. Covered by acceptance #2 + spec. - Timezone:
todaymust be computed in the restaurant time_zone (the presenter already does this viaTime.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