server: pay station + fee calc — full transient loop now passes
computeFee() in @parking/shared: pure integer fee over a TariffStructure (stepped blocks, rolling-24h cap). Two edges fixed under test: grace uses RAW duration (not rounded-up minutes), and the block ladder resets each 24h day. PayStation + routes (GET /api/pay/quote, POST /api/pay): look up the open session, resolve the active tariff version (latest effectiveFrom <= entry), computeFee, append a signed payment event (amount/currency/tender/ tariffVersionId/graceExitMin). overrideMinor handles lost-ticket/dispute. PCI stays out of the app: tender only records cash/card. Verified end to end: entry -> quote (300 for 90min) -> pay -> exit opens and closes the session, verifyChain ok.
This commit is contained in:
@@ -111,8 +111,14 @@ follow this page and [[tariff]]; the decision is recorded in [[session-model]].
|
||||
- **Not a fail-state:** an unpaid reject keeps the barrier closed deliberately (driver returns to
|
||||
the pay station); "exit fails open" ([[fail-state-safety]]) is about the *system* being unable
|
||||
to decide (host/power loss), not an unpaid car.
|
||||
- **Currently every transient exit rejects** — no `payment` events exist until the pay station is
|
||||
built; the validation is the correct end-state, just not passable yet.
|
||||
- **Pay station** (`apps/server/src/pay-station.ts`, routes `GET /api/pay/quote` + `POST /api/pay`):
|
||||
look up the open session → resolve the active tariff version (latest `effectiveFrom ≤ entry`) →
|
||||
`computeFee` → append a signed `payment` event (amount, currency, tender, `tariffVersionId`,
|
||||
`graceExitMin`). An operator `overrideMinor` covers lost-ticket/dispute (recorded as the charged
|
||||
amount + the quoted amount). Pay-on-foot: payment is decoupled from the exit lane. PCI scope stays
|
||||
out of the app — `tender` only records cash/card; card capture is the standalone P2PE terminal.
|
||||
- **The full transient loop now passes end to end** (verified): entry → quote → pay → exit opens,
|
||||
session closed, `verifyChain` ok.
|
||||
|
||||
> **Design gap (flagged):** `lane_devices` has **no entry/exit direction** model. Entry is
|
||||
> button-driven and exit is read-driven, so they don't currently collide — but a lane with both an
|
||||
|
||||
@@ -87,6 +87,15 @@ Deterministic, side-effect-free, unit-testable; the daily cap is applied **per r
|
||||
overnight stay doesn't hit the cap twice). Rounding and segment edges are part of the settled spec
|
||||
because the chain + reconciliation depend on the result being reproducible.
|
||||
|
||||
**Settled edges (2026-06-15, with tests):**
|
||||
- **Grace uses RAW duration** — a stay within `gracePeriodEntryMin` is free even though the
|
||||
increment would round it up (else rounding defeats the grace window).
|
||||
- **The block ladder RESETS each rolling-24h day** — day 2 starts at the first block again (a 25h
|
||||
stay = day-1 capped + day-2 first-hour rate), so the "daily" rate truly resets daily.
|
||||
|
||||
**As-built:** `computeFee(enteredAt, asOf, structure)` in `packages/shared` (pure). Unit-tested
|
||||
across grace, block steps, daily cap, and multi-day reset.
|
||||
|
||||
## The pay-on-foot consequence
|
||||
|
||||
Because payment is decoupled from exit ([[parking-session]] lifecycle), the tariff has **two
|
||||
|
||||
Reference in New Issue
Block a user