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
|
||||
|
||||
+17
@@ -517,3 +517,20 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section).
|
||||
- GAP flagged: lane_devices has no entry/exit DIRECTION model (door mapping hardcoded to 1 for exit);
|
||||
fine while entry=button/exit=read, but multi-reader lanes need a lane-direction/role model (ties to
|
||||
[[open-questions]] #1). Updated [[parking-session]] as-built + gap, [[index]].
|
||||
|
||||
## [2026-06-15] build | Pay station + fee calc; JWT 8h → until-logout
|
||||
- JWT: dropped the 8h `expiresIn` (server.ts global + login). Token now valid **until explicit
|
||||
logout**; cookie maxAge = 30 days so a browser restart doesn't log out an active operator
|
||||
(auth.ts `COOKIE_MAX_AGE_SECONDS`). Closes the pending change from the shift decision; updated
|
||||
[[local-jwt-auth]].
|
||||
- `computeFee(enteredAt, asOf, structure)` in `packages/shared` — pure integer fee calc.
|
||||
TWO BUGS caught by tests: (1) grace must use RAW duration, not the rounded-up minutes (a 10-min
|
||||
stay was being charged a full hour); (2) the block ladder must RESET each rolling-24h day (decision:
|
||||
day 2 restarts at first-block pricing → 25h = 1200 cap + 200). Both fixed; 9 cases pass.
|
||||
- Pay station (`apps/server/src/pay-station.ts` + routes `GET /api/pay/quote`, `POST /api/pay`):
|
||||
open session → active tariff version → computeFee → signed `payment` event (amount/currency/tender/
|
||||
tariffVersionId/graceExitMin); `overrideMinor` for lost-ticket/dispute. Cashier/operator/admin guard.
|
||||
- VERIFIED: full loop entry→quote(300 for 90min)→pay→exit opens+closes, verifyChain ok. (A
|
||||
raw-SQL backdate in one test correctly broke the chain — the tamper-evidence working, not a flow bug.)
|
||||
- Updated [[tariff]] (settled edges + as-built), [[parking-session]] (pay station as-built; full
|
||||
loop passes).
|
||||
|
||||
Reference in New Issue
Block a user