5697137c52
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
56 lines
2.9 KiB
Markdown
56 lines
2.9 KiB
Markdown
---
|
|
type: decision
|
|
tags: [parking, decisions, domain, business]
|
|
sources: []
|
|
updated: 2026-06-15
|
|
status: 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
|
|
|
|
1. **A session is a projection over the signed event log, not a mutable table.** The
|
|
[[append-only-event-chain]] `events` table stays the only source of truth; a
|
|
[[parking-session]] is folded from `vehicle_entry` / `vehicle_exit` / `payment` / `void`
|
|
events. A cache table is allowed for query speed but is always rebuildable and never
|
|
authoritative.
|
|
2. **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]]).
|
|
3. **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|unmanned]] roadmap and sharpens [[open-questions]] #3 toward an unmanned
|
|
pay station (PCI scope still kept out of the app via a certified terminal).
|
|
4. **New signed event types:** `vehicle_entry`, `vehicle_exit`, `payment`, `void` — extend the
|
|
existing `input_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]].
|