Files
parking_solution/wiki/concepts/tariff-time-tiers.md
T
julian 58d8f06ba0 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.
2026-06-18 10:59:21 +02:00

6.3 KiB
Raw Blame History

type, tags, sources, updated, status
type tags sources updated status
concept
parking
domain
business
pricing
design
parksql2017-legacy-schema
2026-06-17 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 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):

{
  "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).