feat(tariff): Tariff Lab — pure session-pricing simulator

Test rates "in time" (overnight windows, daily caps, overstay) in seconds against
any tariff version, instead of waiting hours/days. No real ledger writes.

- Extract priceSession() into @parking/shared: the grace/overstay wrapper over
  computeFee (unpaid -> entry..now; within-grace -> settled 0; grace-expired ->
  overstay, a fresh period from grace-expiry). PayStation.quote() now calls it so
  the booth and the lab can never diverge.
- API (tariffs.ts, tariff:read, read-only): POST /api/tariff/simulate prices a
  hypothetical session (active/any version/inline structure) and returns the
  priceSession outcome + a 30m..3d duration curve (see where the daily cap flattens);
  GET /api/tariff/simulate/session/:identity prefills from a real ledger session.
- UI TariffLab.tsx at Setup -> "Tariff Lab": version picker, entry/asOf times,
  optional payment+grace, category, and load-a-real-ticket. Admin-gated, available
  on-site (useful to quote a dispute).
- 4 new priceSession unit tests incl. the ticket-1245791632490 overstay-not-zero
  regression (40 pass). i18n lab.* + nav.tariffLab (sq+en). Verified live via the UI.

Wiki: tariff (priceSession + Tariff Lab as-built), log.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-20 12:05:30 +02:00
parent a4712774ab
commit 3d02134711
11 changed files with 669 additions and 17 deletions
+19
View File
@@ -1003,3 +1003,22 @@ pay modal: "New period due"/OVERSTAY; handlePayAndExit now charges when canPay (
!alreadyPaid — would have skipped the overstay charge). i18n pay.overstay/overstayHint/topUp +
booth.badgeOverstay*/fStatusOverstay rewritten in sq+en. Build+lint green. Updated [[booth-exit-flow]]
(overstay section + naming history + partial-resolution note on the grace-renewal open question).
## [2026-06-20] feat | Tariff Lab — pure session-pricing simulator (test rates in time)
The tariff engine is pure but could only be EXERCISED by waiting (booth reads real
wall-clock). Added a simulator. Extracted priceSession(enteredAt, asOf, structure,
payments[], category?) into @parking/shared — the grace/overstay wrapper over
computeFee (unpaid→entry→now; within-grace→settled 0; grace-expired→overstay new
period from grace-expiry). PayStation.quote() now calls it, so booth + lab can't
diverge. New routes (tariffs.ts, tariff:read, no ledger writes): POST
/api/tariff/simulate (price a hypothetical session vs active/any version/inline
structure; returns priceSession outcome + a 30m..3d duration curve) and GET
/api/tariff/simulate/session/:identity (prefill from a real ledger session). UI
apps/web/src/TariffLab.tsx at Setup→"Tariff Lab": version picker, entry/asOf, optional
payment+grace, category, load-a-ticket; shows amount due, billed period, overstay/
settled, curve. i18n lab.* + nav.tariffLab (sq+en). 4 new priceSession unit tests
incl. the ticket-1245791632490 overstay-not-zero regression (40 tests pass). Verified
live via the real UI: a 3h stay → ALL 3,000, curve shows the daily cap flattening at
6h and multi-day stepping; ticket-load returned a real session. Build+lint green.
Updated [[tariff]] + [[booth-exit-flow]].