Files
julian 5697137c52 feat(subscription): rename permit→subscription + monthly pricing
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
2026-06-18 13:15:04 +02:00

2.9 KiB

type, tags, sources, updated, status
type tags sources updated status
decision
parking
decisions
domain
business
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

  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 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