docs(wiki): tariff research — legacy ParkSQL2017 schema, time-tiers & validation/sponsorship design

Ingest the predecessor SQL Server schema (raw + source summary) and file design
pages for time-of-day/seasonal tariff tiers and merchant validation/postpaid
sponsorship. Cross-link tariff.md and validation-discounts.md. No code.
This commit is contained in:
2026-06-18 10:59:21 +02:00
parent 71aaad03b9
commit 58d8f06ba0
6 changed files with 1582 additions and 3 deletions
+115
View File
@@ -0,0 +1,115 @@
---
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).