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
This commit is contained in:
@@ -14,7 +14,7 @@ session projection.
|
||||
|
||||
## The rule
|
||||
|
||||
An identity (ticket id, [[permit]] credential, or plate) **must not enter while it already has an
|
||||
An identity (ticket id, [[subscription]] credential, or plate) **must not enter while it already has an
|
||||
OPEN [[parking-session|session]].** At entry:
|
||||
|
||||
```
|
||||
@@ -25,13 +25,13 @@ identify vehicle → is there already an OPEN session for this id?
|
||||
|
||||
This is a **fold over the signed [[append-only-event-chain]]** ("does an entry for this id exist
|
||||
with no matching exit?") — not a mutable in/out flag that could be edited. Same projection that
|
||||
powers [[capacity-occupancy]] and [[permit]] `maxConcurrent`.
|
||||
powers [[capacity-occupancy]] and [[subscription]] `maxConcurrent`.
|
||||
|
||||
## Interaction with the limits already designed
|
||||
|
||||
- **Transient ticket** — a single ticket id is inherently one session; a second entry on the same
|
||||
id is always a violation (or a re-print/duplication attempt).
|
||||
- **Permit** — passback is the *per-car* case of the permit's `maxConcurrent` ([[permit]]): a
|
||||
- **Permit** — passback is the *per-car* case of the permit's `maxConcurrent` ([[subscription]]): a
|
||||
multi-car permit legitimately has several open sessions, but **the same car/credential** entering
|
||||
twice is still a violation. So enforce per-identity, *under* the permit's concurrency allowance.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ editable and drifts; the chain is the truth). Spaces-free = `capacity − occupa
|
||||
|
||||
- **`capacity`** is admin-set per site (and per **zone/level** if the lot has sections — model a
|
||||
`zone` on capacity + on the entry so multi-level is a later addition, not a rewrite).
|
||||
- Permit concurrency (`maxConcurrent`, see [[permit]]) is the same kind of fold, scoped to one
|
||||
- Permit concurrency (`maxConcurrent`, see [[subscription]]) is the same kind of fold, scoped to one
|
||||
permit's open sessions.
|
||||
|
||||
## Full → refuse entry + FULL sign
|
||||
|
||||
@@ -43,7 +43,7 @@ A session needs a key that survives from entry to exit. Two populations, two key
|
||||
|
||||
- **Transient:** a **ticket id** (printed, ideally on pre-numbered stock — see [[reconciliation]])
|
||||
or a **plate** read by [[lpr-camera|LPR]]. This id is carried in the event's `identity` field.
|
||||
- **Permit holder:** a **credential** (card / plate / QR) matched to a [[permit]] record. A valid
|
||||
- **Permit holder:** a **credential** (card / plate / QR) matched to a [[subscription]] record. A valid
|
||||
permit means the session owes nothing — the PAY step is skipped (see below).
|
||||
|
||||
## Lifecycle (pay-on-foot / pay station model)
|
||||
@@ -71,7 +71,7 @@ States, as derived from events:
|
||||
| **CLOSED** | a matching `vehicle_exit` event exists |
|
||||
| **VOIDED** | a `void` event references the session (lost ticket written off, error correction) |
|
||||
|
||||
Permit sessions skip PAID: a valid [[permit]] at exit is itself the authorization to close.
|
||||
Permit sessions skip PAID: a valid [[subscription]] at exit is itself the authorization to close.
|
||||
|
||||
## Edge cases the model must name (not yet designed in full)
|
||||
|
||||
@@ -106,7 +106,7 @@ follow this page and [[tariff]]; the decision is recorded in [[session-model]].
|
||||
fails. See [[device-input-flow]].
|
||||
- **Read dispatch** (`apps/server/src/read-dispatch.ts`): a credential read routes to the
|
||||
**permit flow** if it matches a permit (card/QR/bound plate), else to the transient **exit flow**.
|
||||
Lane resolved once (`readerLaneWithAccess`). See [[permit]] as-built.
|
||||
Lane resolved once (`readerLaneWithAccess`). See [[subscription]] as-built.
|
||||
- **Exit flow** (`apps/server/src/exit-flow.ts`): a credential **read** (the `read` bus channel) →
|
||||
fold the signed ledger for that identity → validate **open + PAID + within `gracePeriodExitMin`**
|
||||
→ signed `vehicle_exit` → `pulseOpen`. Unpaid / expired / unknown → signed `anomaly`, barrier
|
||||
|
||||
@@ -17,7 +17,7 @@ derived and rebuildable, never a separate ledger.
|
||||
- **Revenue** — by day/week/shift, by tender (cash vs. card), gross vs. discounts vs. net. Source:
|
||||
`payment` events + [[validation-discounts|discount]] events + `shift_z_report` ([[shift]]).
|
||||
- **Occupancy** — current ([[capacity-occupancy]]) and historical curve; peak times; turnover.
|
||||
- **Stay analytics** — average/median duration, distribution; transient vs. [[permit]] split.
|
||||
- **Stay analytics** — average/median duration, distribution; transient vs. [[subscription]] split.
|
||||
- **Permit usage** — active permits, utilisation, concurrency vs. `maxConcurrent`.
|
||||
- **Anomalies** — out-of-band opens, never-exited sessions, occupancy drift, over-validation —
|
||||
the `anomaly` events + reconciliation findings ([[reconciliation]]).
|
||||
|
||||
@@ -63,7 +63,9 @@ login ————————————————————————
|
||||
## What End Shift does
|
||||
|
||||
1. Determine the shift's payment set: the signed `payment` events ([[parking-session]],
|
||||
[[append-only-event-chain]]) between this shift's start mark and now.
|
||||
[[append-only-event-chain]]) between this shift's start mark and now. This includes a
|
||||
**[[subscription]] fee** an operator collects during the shift (sold/renewed at the booth → a
|
||||
signed `payment`, deferred build) — it folds into this set like any transient taking.
|
||||
2. Sum by **tender**: `cashTotal`, and `cardTotal` from the POS/terminal **if a POS is configured**
|
||||
(the card line is omitted when there's no terminal).
|
||||
3. Append a signed **`shift_z_report`** event (type already in `packages/shared`): `{ operator,
|
||||
|
||||
@@ -10,7 +10,7 @@ status: open
|
||||
|
||||
How a [[parking-session]]'s fee is computed from its duration. A tariff is **admin-composed data,
|
||||
not code** — the park owner builds and constantly edits the rate card at runtime (like a
|
||||
[[permit]]), in a selectable currency, with **no numbers hard-coded anywhere** and no code change to
|
||||
[[subscription]]), in a selectable currency, with **no numbers hard-coded anywhere** and no code change to
|
||||
reprice. The computation is **pure and offline** ([[offline-first]]: no network, no clock authority
|
||||
beyond the host).
|
||||
|
||||
@@ -144,9 +144,9 @@ production.
|
||||
|
||||
## Permit holders
|
||||
|
||||
A valid [[permit]] bypasses tariff computation entirely for the covered period (subscription
|
||||
A valid [[subscription]] bypasses tariff computation entirely for the covered period (subscription
|
||||
already paid out-of-band). A permit that has lapsed mid-stay falls back to the transient tariff for
|
||||
the uncovered time — an edge case to design with [[permit]].
|
||||
the uncovered time — an edge case to design with [[subscription]].
|
||||
|
||||
## Versioning — edits publish immutable, effective-dated versions
|
||||
|
||||
@@ -198,7 +198,7 @@ Two operator asks extend this engine; both have design pages (not yet built), gr
|
||||
slicing a stay at window boundaries while keeping the block ladder + daily cap continuous.
|
||||
- **Validation & sponsorship** (merchant comps, coupons, **postpaid B2B** "enter/exit free, bill the
|
||||
business monthly") — see [[validation-sponsorship]]. A validation is a **typed modifier applied as a
|
||||
signed event** on a transient session, distinct from a [[permit]]; postpaid sponsors accrue a
|
||||
signed event** on a transient session, distinct from a [[subscription]]; postpaid sponsors accrue a
|
||||
monthly-invoiced liability derivable from the chain.
|
||||
|
||||
## Open
|
||||
|
||||
@@ -19,7 +19,7 @@ postpaid agreement whose customers enter and exit freely, billed to the business
|
||||
|
||||
## Why this is NOT a permit (the key distinction)
|
||||
|
||||
| | [[permit]] | Validation / sponsorship |
|
||||
| | [[subscription]] | Validation / sponsorship |
|
||||
| --- | --- | --- |
|
||||
| Subject | Known in advance; carries a credential (card/QR/plate) | Anonymous walk-in; identified only by the **ticket they were issued** |
|
||||
| When applied | At entry (credential opens the lane) | **After entry**, against an existing session — at a pay station, by a code, or by a sponsor rule |
|
||||
@@ -74,7 +74,7 @@ validations id, session_id, sponsor_id?, type, amount_minor|minutes,
|
||||
- A **postpaid** sponsor: each full-comp validation appends a row and accrues `amount` to the
|
||||
sponsor; monthly invoice = sum over the period; exit is free at the lane.
|
||||
- **Free entry/exit "freely"**: either the sponsor issues credentials (then it's closer to a
|
||||
[[permit]] — pick that path), or customers take a normal ticket and a sponsor rule / merchant code
|
||||
[[subscription]] — pick that path), or customers take a normal ticket and a sponsor rule / merchant code
|
||||
comps it at exit. The agreement wording decides which; **both are expressible.**
|
||||
|
||||
## Reconciliation & settlement
|
||||
@@ -83,7 +83,7 @@ validations id, session_id, sponsor_id?, type, amount_minor|minutes,
|
||||
Statement lines trace to signed validation events → disputes resolvable against the chain.
|
||||
|
||||
## Open
|
||||
- **"Enter/exit freely" mechanism**: sponsor-issued credentials ([[permit]]-like) vs. ticket +
|
||||
- **"Enter/exit freely" mechanism**: sponsor-issued credentials ([[subscription]]-like) vs. ticket +
|
||||
comp-at-exit. Likely offer both; confirm the operator's actual deal shape.
|
||||
- Prepaid coupon format: printed codes (legacy) vs. QR vs. merchant web-validation portal.
|
||||
- Who may apply a validation, and the **per-operator cap** (a comp is a fraud vector — bound it and
|
||||
|
||||
Reference in New Issue
Block a user