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:
@@ -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).
|
||||
+32
-2
@@ -124,6 +124,24 @@ time references**, not one:
|
||||
`gracePeriodExit` is therefore a real revenue/UX parameter, not a nicety: too short traps people
|
||||
who paid; too long gives free parking between pay and exit.
|
||||
|
||||
> **As-built correction (2026-06-17):** the overstay top-up reprices from **entry**, not `paidAt` —
|
||||
> `computeFee(enteredAt, now, …)` (so the timer never restarts; the customer pays the true entry→now
|
||||
> total). The line above (`f(paidAt, now, …)`) was the original sketch; the implementation uses entry.
|
||||
|
||||
### ⚠ Open question — walk-back grace renews on every payment
|
||||
|
||||
A consequence of the two-time-reference model, surfaced via the [[booth-exit-flow|booth exit /
|
||||
voucher]] path: every `payment` event stores its own `gracePeriodExit`, and the exit check reads the
|
||||
**latest** payment's value. So an **overstay top-up re-grants a full, fresh grace window** each time.
|
||||
The fee is correct (always recomputed from entry — no free exit), but the **walk-back grace doubles**
|
||||
(or repeats) on every top-up — a customer could pay → wait → pay a tiny delta → earn another window →
|
||||
repeat. The leak is **time, not money**, bounded by increment coarseness but real.
|
||||
|
||||
Candidate policies (business call): grant grace on a top-up **only when it charged new money**
|
||||
(recommended), a **single non-renewing window** from the first payment, or a **per-session grace
|
||||
cap**. Full analysis + the decided/undecided halves live in [[booth-exit-flow]]. Pick a policy before
|
||||
production.
|
||||
|
||||
## Permit holders
|
||||
|
||||
A valid [[permit]] bypasses tariff computation entirely for the covered period (subscription
|
||||
@@ -169,12 +187,24 @@ Unlike the event log, tariff data is **mutable master data** in the sense that n
|
||||
on the network — [[offline-first]]), a base currency, and a rounding policy. Deferred to
|
||||
[[open-questions]].
|
||||
|
||||
## Extensions under design
|
||||
|
||||
Two operator asks extend this engine; both have design pages (not yet built), grounded in
|
||||
[[parksql2017-legacy-schema|the legacy schema]] + external research:
|
||||
|
||||
- **Time-of-day / weekday / seasonal tiers** (happy hour, off-peak, weekend, vehicle category) —
|
||||
see [[tariff-time-tiers]]. Chosen shape: **time-windowed rate cards** selected by wall-clock window,
|
||||
layered additively on this structure (a bare default card = today's behaviour). The hard part is
|
||||
slicing a stay at window boundaries while keeping the block ladder + daily cap continuous.
|
||||
- **Validation & sponsorship** (merchant comps, coupons, **postpaid B2B** "enter/exit free, bill the
|
||||
business monthly") — see [[validation-sponsorship]]. A validation is a **typed modifier applied as a
|
||||
signed event** on a transient session, distinct from a [[permit]]; postpaid sponsors accrue a
|
||||
monthly-invoiced liability derivable from the chain.
|
||||
|
||||
## Open
|
||||
|
||||
- The **actual rate cards** are owner-authored at runtime — nothing to confirm at build time; the
|
||||
composer UI + validation (sane blocks, non-negative, ordered `uptoMin`) is the work.
|
||||
- **Time-of-day / weekday tiers** — not in the block model yet; add as a tier wrapper if a site
|
||||
needs day/night/weekend cards (deferred until asked).
|
||||
- **Blank-tariff policy** — free vs. gated until a rate card is published (operator policy).
|
||||
- **In-progress version-boundary** — entry-version (decided) vs. pro-rate (revisit if needed).
|
||||
- **FX** — exchange-rate system, offline rate source, base currency ([[open-questions]]).
|
||||
|
||||
@@ -39,8 +39,17 @@ Because each validation is signed and attributed (`issuedBy`), over-validation b
|
||||
merchant is **visible to [[reconciliation]]** (a merchant validating far more than their footfall is
|
||||
an anomaly), rather than invisible free parking.
|
||||
|
||||
## Postpaid sponsors
|
||||
|
||||
When the validating party is a **business with a postpaid agreement** (its customers park free, it's
|
||||
billed monthly) — not just a one-off discount — the **sponsor account + settlement** layer is in
|
||||
[[validation-sponsorship]]. That's the distinction between a discount (this page) and a *sponsored*
|
||||
session that accrues a receivable.
|
||||
|
||||
## Open
|
||||
|
||||
- Validation types the site needs (free hours / fixed amount / percentage / flat rate).
|
||||
- Validation types the site needs (free hours / fixed amount / percentage / flat rate) — superset in
|
||||
[[validation-sponsorship]] (`comp`/`percent`/`fixed`/`time-credit`/`rate-switch`).
|
||||
- Whether merchants self-serve (portal/terminal) or the operator applies it.
|
||||
- Caps (max discount, max per merchant/day).
|
||||
- Prepaid coupon pool vs. postpaid accrual — see [[validation-sponsorship]].
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
type: concept
|
||||
tags: [parking, domain, business, pricing, validation, design]
|
||||
sources: [parksql2017-legacy-schema]
|
||||
updated: 2026-06-17
|
||||
status: open
|
||||
---
|
||||
|
||||
# Validation & Sponsorship — merchant comps, coupons, postpaid B2B
|
||||
|
||||
Builds on [[validation-discounts]] (the signed-event discount mechanism) to add the layer it leaves
|
||||
open: **a sponsor account and postpaid B2B billing.** The driving case — **a nearby business with a
|
||||
postpaid agreement whose customers enter and exit freely, billed to the business monthly.**
|
||||
|
||||
> This page owns the **sponsor/account/settlement** model and the **permit-vs-validation
|
||||
> distinction**. The *how a discount is applied* mechanics (signed event, `due = max(0, fee −
|
||||
> discounts)`, attribution, anti-abuse) live in [[validation-discounts]] — not duplicated here.
|
||||
> Status: **design, not built.**
|
||||
|
||||
## Why this is NOT a permit (the key distinction)
|
||||
|
||||
| | [[permit]] | Validation / sponsorship |
|
||||
| --- | --- | --- |
|
||||
| Subject | Known in advance; carries a credential (card/QR/plate) | Anonymous walk-in; identified only by the **ticket they were issued** |
|
||||
| When applied | At entry (credential opens the lane) | **After entry**, against an existing session — at a pay station, by a code, or by a sponsor rule |
|
||||
| Who pays | The subscriber, out-of-band | A **third party** (merchant/sponsor), or nobody (comp) |
|
||||
| Model fit | `permits` + credentials | New: a **validation event** on a session + a **sponsor account** |
|
||||
|
||||
A permit bypasses tariff computation; a validation **adjusts the computed fee** (or zeroes it). They
|
||||
compose — but they are different primitives.
|
||||
|
||||
## Two economic models (both real)
|
||||
|
||||
- **Prepaid** — merchant buys a pool of value up front (legacy `BA_Cupons`: printed single-use codes
|
||||
worth `DiscMinutes`; City Center research: merchant pre-buys time tickets 15 min→all-day).
|
||||
Reconciliation = count used codes against the pool.
|
||||
- **Postpaid** (the asked-for case) — merchant signs an agreement; their customers park free or
|
||||
discounted; the system **accrues each validation against a sponsor balance** and **invoices monthly**
|
||||
(City Center: "billed for the number of tickets validated each month," verified 3-0). No money moves
|
||||
at the lane.
|
||||
|
||||
The legacy system did **only prepaid coupons** — **the postpaid sponsor account is net-new** for this
|
||||
project.
|
||||
|
||||
## Modifier types (extends [[validation-discounts]])
|
||||
|
||||
The discount-type enum lives in [[validation-discounts]]; legacy `DiscType` (smallint) and research
|
||||
(Amano McGann / HUB J4M, abstained-not-refuted) confirm the set: `comp` / `percent` / `fixed` /
|
||||
`time-credit` (legacy `DiscMinutes`) / `rate-switch`. **Sponsorship adds one field** to a validation:
|
||||
a `sponsorId`. Full-comp + a sponsor = the "free entry/exit, bill the business" case.
|
||||
|
||||
## The sponsor-liability consequence (anti-fraud)
|
||||
|
||||
The validation is a signed event ([[validation-discounts]], [[append-only-event-chain]]); what
|
||||
**sponsorship** adds is that **free-to-the-parker is not free-to-the-ledger** — it is a *receivable
|
||||
from the sponsor*. Under the [[threat-model|operator-as-adversary]] model:
|
||||
|
||||
- A postpaid sponsor's "enter/exit freely" still **mints signed entry + exit events** (and snapshots)
|
||||
— the audit trail is identical to a paying car; only the **settlement target** differs.
|
||||
- The **sponsor's period liability = the sum of `sponsorId`-tagged validation events** over the
|
||||
period — derivable from the chain, reconcilable like a [[shift|shift Z-report]] and visible to
|
||||
[[reconciliation]] (a sponsor comping far more than plausible footfall is an anomaly).
|
||||
|
||||
## Proposed data shape (illustrative — design only)
|
||||
|
||||
```
|
||||
sponsors id, name, contact, mode {prepaid|postpaid},
|
||||
balance_minor (prepaid pool / postpaid accrual), billing_period, active
|
||||
validations id, session_id, sponsor_id?, type, amount_minor|minutes,
|
||||
code?, operator_id, created_at // append-only; one row per application
|
||||
(coupons) code, value_minutes|minor, single_use, used_at? // prepaid pool, optional
|
||||
```
|
||||
|
||||
- A **postpaid** sponsor: each full-comp validation appends a row and accrues `amount` to the
|
||||
sponsor; monthly invoice = sum over the period; exit is free at the lane.
|
||||
- **Free entry/exit "freely"**: either the sponsor issues credentials (then it's closer to a
|
||||
[[permit]] — pick that path), or customers take a normal ticket and a sponsor rule / merchant code
|
||||
comps it at exit. The agreement wording decides which; **both are expressible.**
|
||||
|
||||
## Reconciliation & settlement
|
||||
- **Prepaid**: pool decrements; alert at low balance; no invoice.
|
||||
- **Postpaid**: accrue; **monthly statement** per sponsor (legacy/City Center cadence ~the 10th).
|
||||
Statement lines trace to signed validation events → disputes resolvable against the chain.
|
||||
|
||||
## Open
|
||||
- **"Enter/exit freely" mechanism**: sponsor-issued credentials ([[permit]]-like) vs. ticket +
|
||||
comp-at-exit. Likely offer both; confirm the operator's actual deal shape.
|
||||
- Prepaid coupon format: printed codes (legacy) vs. QR vs. merchant web-validation portal.
|
||||
- Who may apply a validation, and the **per-operator cap** (a comp is a fraud vector — bound it and
|
||||
always sign it).
|
||||
- Invoicing: in-app statement only, or export for external billing? FX if sponsor bills in another
|
||||
currency (defer to [[tariff]] FX).
|
||||
- Partial-stay sponsorship (merchant covers first 2h, parker pays the rest) — `time-credit` or
|
||||
`rate-switch` covers it; confirm.
|
||||
Reference in New Issue
Block a user