The "permit/lejet" feature is really a subscription. Full rename of the mutable master data, plus a recurring monthly price. - DB (migration 0004, data-preserving ALTER RENAME): permits→subscriptions, permit_credentials/_plates→subscription_*, sessions.permit_id→subscription_id. - Pricing: per-subscription priceMinor + period(monthly) + currency, with a site default (site_config.subscription_monthly_price_minor) pre-filling the form. - Server: subscription-flow.ts (SubscriptionFlow), routes/subscriptions.ts (/api/subscriptions). Web: SubscriptionManager, route, i18n (sq Abonimet/en). - The signed ledger `permitId` payload is intentionally kept — immutable hash-chained history; renaming it would break verification of past events. Deferred (wiki notes): fee collection into the ledger/shift (a shift-attributed payment), LPR/ANPR plate source, time-of-day access windows (overnight subscriber). Also carries the device-footer UI surface (api DeviceStatus, router mount, i18n devices) due to shared-file overlap with the preceding footer commit. Verified end-to-end on a fresh DB and migration on a live-DB copy (sessions preserved). Live DB migrated. Full monorepo builds clean. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2.9 KiB
type, tags, sources, updated, status
| type | tags | sources | updated | status | ||||
|---|---|---|---|---|---|---|---|---|
| decision |
|
2026-06-15 | open |
Decision: Parking Session Model
The starting decision for the business layer, taken 2026-06-15 as the project pivots from the (now hardware-verified) device/integrity layer to the parking operation.
Decisions
- A session is a projection over the signed event log, not a mutable table. The
append-only-event-chain
eventstable stays the only source of truth; a parking-session is folded fromvehicle_entry/vehicle_exit/payment/voidevents. A cache table is allowed for query speed but is always rebuildable and never authoritative. - Transient-first, mixed site. Model the casual pay-for-duration session + tariff first; layer subscription holders on top as a second identity source that short-circuits payment (entry-exit-readers).
- Pay-on-foot / pay station. Payment is decoupled from exit: the customer pays at a central station; the exit lane only validates the session is paid and within the walk-back grace window before opening (parking-session lifecycle). Matches the autonomous-direction roadmap and sharpens open-questions #3 toward an unmanned pay station (PCI scope still kept out of the app via a certified terminal).
- New signed event types:
vehicle_entry,vehicle_exit,payment,void— extend the existinginput_received. Recorded in append-only-event-chain.
Why (rejected alternative)
A mutable sessions table carrying amountOwed / paidStatus as the source of truth was
rejected: it reopens the exact fraud vector the system exists to close (threat-model — the
insider edits the row, marks it paid, pockets the cash). Making "paid" a signed payment
event means it can't be forged and can't be silently deleted (a deletion breaks the chain). The
projection approach costs a fold/cache but keeps the anti-fraud guarantee intact end-to-end.
What this unblocks
Closes the dangling thread from device-input-flow ("the entry flow itself is the next
build"): input_received → signed vehicle_entry → ticket print → pulseOpen, then the
pay-station and exit-validation flows. Schema (packages/db) + shared types follow the
parking-session + tariff design pages.
Open / next
- Rate card, currency, grace windows, caps — operator/procurement input (tariff).
- Tariff versioning (effective-dated) for historical repricing.
- subscription data model + lapsed-mid-stay handling.
- Wire payment capture to a concrete pay-station terminal (open-questions #3) — kept abstract (payment = an independent signed event referencing a session) until procurement settles.
- Reconciliation of sessions/payments against an external authority remains open-questions #4
- the unbuilt witness/reconciliation gap in append-only-event-chain.