feat(tariff): V2 — legacy-parity pricing (time-of-day, category, seasonal, flat)
Bring the legacy ParkSQL2017 pricing BREADTH onto our engine while keeping
integer-minor-unit money + immutable signed versions (rejecting legacy's
float money / mutable rows). TariffStructure becomes a discriminated union:
V1 = the original bare ladder (UNCHANGED, verbatim algorithm, golden-
regression-tested against the live version); V2 = {version:2, tz, shared
knobs, defaultCard, windowedCards[]} where each card is flat OR a block
ladder and may be scoped by wall-clock hour window / day-of-week / date
range / vehicle category.
computeFeeV2 prices by stepping one increment at a time, advancing the
ladder by ELAPSED minutes (continuous) while selecting the active card by
WALL-CLOCK time in the version's FROZEN tz. Decisions: tz is a per-site
setting (site_config.timezone, default Europe/Tirane) stamped server-side
into each version on publish — never the host clock (reproducibility);
default-card cap governs a mixed day; precedence = specificity
(date>dow>hour) -> priority -> name (total, order-independent), validation
rejects ambiguous ties; category = a card FIELD, frozen in the signed
vehicle_entry payload (site_config.default_vehicle_category default), read
at both pricing call-sites.
Composer: default card front-and-centre (flat/ladder toggle), tiers under
an "Advanced" disclosure; emits BARE V1 when no tiers (back-compat). DB:
migrations 0005 (timezone) + 0006 (default_vehicle_category). Stood up
vitest in @parking/shared (was zero tests on the ledger-feeding fee fn);
36 tests incl. golden V1 regression, happy-hour/overnight/dow/flat/category/
cap edges, precedence shuffle-invariance, Europe/Tirane DST determinism,
validation matrix — all green. No event-chain change.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE `site_config` ADD `timezone` text;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `site_config` ADD `default_vehicle_category` text;
|
||||
@@ -36,6 +36,20 @@
|
||||
"when": 1781800000000,
|
||||
"tag": "0004_subscriptions_rename",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1781884800000,
|
||||
"tag": "0005_site_timezone",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1781884900000,
|
||||
"tag": "0006_site_default_category",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -171,6 +171,19 @@ export const siteConfig = sqliteTable("site_config", {
|
||||
* own price and may differ. null = no site default set. See
|
||||
* wiki/entities/subscription.md. */
|
||||
subscriptionMonthlyPriceMinor: integer("subscription_monthly_price_minor"),
|
||||
/** IANA timezone the site operates in (e.g. "Europe/Tirane"). Used to evaluate a
|
||||
* tariff's wall-clock pricing windows (happy hour / night / seasonal). COPIED into
|
||||
* each published tariff version's structure.tz so the windows are frozen/immutable
|
||||
* per version — historical sessions reprice deterministically regardless of any
|
||||
* later config change. null/absent ⇒ default "Europe/Tirane" at publish time.
|
||||
* See wiki/concepts/tariff-time-tiers.md. */
|
||||
timezone: text("timezone"),
|
||||
/** Default vehicle/customer category assigned to a transient entry when none is
|
||||
* captured at the lane (every transient today). Operator policy — a plain car park
|
||||
* leaves it "default"; a mixed lot might set "car". Frozen into each vehicle_entry
|
||||
* payload so V2 category pricing reprices identically at exit. null ⇒ the shared
|
||||
* DEFAULT_VEHICLE_CATEGORY fallback. See wiki/concepts/tariff-time-tiers.md. */
|
||||
defaultVehicleCategory: text("default_vehicle_category"),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
|
||||
Reference in New Issue
Block a user