--- type: concept tags: [parking, domain, business, pricing, design] sources: [parksql2017-legacy-schema] updated: 2026-06-17 status: open --- # Tariff Time Tiers — happy hour, off-peak, weekend, seasonal Design for **time-of-day / day-of-week / seasonal pricing** on top of the existing [[tariff]] engine. Resolves the `tariff.md` open question *"Time-of-day / weekday tiers — not in the block model yet."* Driven by two concrete operator asks: a **happy-hour** rate, and (from [[parksql2017-legacy-schema|the legacy schema]]) **vehicle/customer categories**. > Status: **design, not built.** No schema/code committed yet — this records the chosen shape and > the rejected alternatives so implementation is a transcription. ## The two real-world models we looked at 1. **Legacy `BA_TicketPrice`** ([[parksql2017-legacy-schema]]): each rate-card row is scoped by `ValidFrom`/`ValidTo` (date window) **and** `ValidFromHour`/`ValidToHour` (daily hour window) **and** `TicketCategoryID`. Happy hour = a second price row valid 14:00–16:00. Off-peak/season = a row with a date or hour window. The active rate is selected by **(category, now-or-entry, date)**. 2. **Research (verified):** rates modelled as **time segments nested inside recurring time frames**, where time frames = days-of-week / holidays / special-event days (US patent 10,762,723, 3-0 verified). Industry APIs (INRIX `structured_rate`) carry `time_in`/`time_out` + `dow` per rate. Both point at the **same primitive**: a rate that is *active for a wall-clock window*. Both converge: **happy hour is not a discount flag — it is a selector over which rate card is active for a given slice of wall-clock time.** ## The decision to make: which-rate selector vs. discount modifier | Option | Shape | Verdict | | --- | --- | --- | | **A. Time-windowed rate cards** (recommended) | A stay is sliced at wall-clock boundaries; each slice priced by the rate card whose window covers it. Happy hour = a card with `window: {dow, fromHour, toHour}`. | Most general: one mechanism covers happy hour, early-bird, night flat, weekend, season. Matches both references. | | **B. Discount modifier on one ladder** | Keep one ladder; apply `−X%`/`−N min` when the clock is inside a window. | Simpler, but can't express "different ladder at night," daily caps interact badly, and it's a second pricing path. Rejected as the primary model. | **Recommendation: A.** A discount-style happy hour (B) is then expressible *as* a windowed card (a cheaper ladder), so we don't lose it. ## The wall-clock slicing consequence (the hard part) The current `computeFee(enteredAt, asOf, structure)` walks **elapsed** minutes through `blocks`. Time tiers add a **second clock**: the *wall-clock* time-of-day, which the elapsed walk doesn't track. A stay 13:30→15:30 that has happy hour 14:00–16:00 must be **split at 14:00**: 30 min normal + 90 min happy. So the fee function must: 1. Resolve the **applicable rate set** for the stay (all cards matching the category, ordered by precedence — see below). 2. Walk the stay in wall-clock order, **switching the active card at each window boundary**, while keeping the **elapsed-duration position** in the block ladder continuous (so block steps and the daily cap still accrue across a window switch — a happy hour mid-stay must not reset the ladder). 3. Keep it **pure, integer, offline, deterministic** — the same invariants the current engine and the [[append-only-event-chain|signed chain]] depend on. The `payment` event still records the `tariffVersionId`; the version now contains the windowed card set, so a past session reprices identically. > Open edge: does the block ladder accrue by **elapsed time** (a 2h stay is in the 2nd block > regardless of windows) or **reset per window**? Legacy `IntervalChange` hints some sites reset. > **Lean: elapsed-continuous** (predictable, no double-charging), revisit if a site needs otherwise. ## Precedence (when windows overlap) Multiple cards can match one instant (a weekday-evening card + a holiday card). Need a deterministic winner. Proposal, most-specific-wins, matching the research's "event rates override": `special-event/holiday > specific date range > day-of-week + hour > hour-only > default`. Ties broken by an explicit integer `priority`. This must be **total and pure** — no ambiguity the operator can't predict, no "depends on row order." ## Vehicle / customer category (the second new axis) Legacy `BA_TicketCategory` prices by **category** (car/bus/VIP/…), orthogonal to time. Two ways: - **Multiple tariffs scoped by category** — the schema already reserves `tariffs.scope` (`site`/`zone`); add `category` cleanly, no migration. The session records which category it was priced under. - **Category as another window dimension** on the card. Simpler table, busier card. **Lean: category as a tariff scope** (a category is a different rate *card*, not a different *window* of one). Deferred until a site actually needs non-car pricing, but the `scope` hook means **no migration when it lands**. ## Proposed data shape (illustrative) Extend the `TariffStructure` JSON (still one immutable [[tariff]] version) with an optional ordered card list; absence = today's single-ladder behaviour (back-compatible): ```jsonc { "currency": "ALL", "defaultCard": { /* the existing blocks/cap/grace structure */ }, "windowedCards": [ { "name": "Happy hour", "priority": 10, "window": { "dow": [1,2,3,4,5], "fromHour": "14:00", "toHour": "16:00" }, "blocks": [ /* cheaper ladder */ ], "dailyCapMinor": null } ] } ``` A bare `defaultCard` (no `windowedCards`) is exactly today's tariff — so this ships additively and a site that never wants tiers never sees them. Keeps the **intuitive-for-operators** goal: the common case stays one rate card; tiers are opt-in. ## Open - Elapsed-continuous vs. per-window ladder reset (lean: elapsed-continuous). - Holiday/special-event calendar: a date list per version, or a separate editable calendar table? - Precedence model — confirm most-specific + explicit `priority` tiebreak. - Category axis — confirm "category = tariff scope" vs. window dimension (deferred). - UI: how to author windows without confusing operators (the notoriously-hard part — keep default card front-and-center, tiers as an "advanced" add).