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:
+20
@@ -800,3 +800,23 @@ Audited wiki vs. the session: three major builds (live WebSocket, frontend found
|
||||
## [2026-06-18] ingest | Shift gating — site-wide single-open, booth money-path gate, per-shift logs
|
||||
|
||||
Built the shift-enforcement model. A shift is now **site-wide single-open** (was per-operator): `ShiftService.currentOpenShift()` reads the most recent shift event on the whole chain; `open()` refuses if ANY shift is open and throws `ShiftAlreadyOpenError{heldBy}`. Login stays decoupled from shifts (operator can log in off-shift to review). The booth money path is **gated**: `/api/pay`, `/api/exit`, `/api/voucher`, `/api/barrier/reopen` get a `requireShift` preHandler → 409 `{code:"no_shift"}`; read-only lookups stay open so the modal can display + prompt. `GET /api/shift/current` now returns the site-wide `{open:{startedAt,operator},isMine}`. Logs are **per-shift** via `GET /api/events?since=<shiftStart>`. UI: header shift button (open / close-mine / disabled-when-other), pay-modal gate banner with one-click open, gated Active-Sessions re-open, shift-scoped live feed; shared `useShift()` Query invalidated by the WS on shift/cash events. Updated [[shift]] (new "Site-wide single-open + booth gate" section; superseded the per-operator as-built note) and [[booth-console]] (header control + gate). Verified the invariant + chain integrity on a fresh migrated DB (11/11 assertions). Builds clean across db/server/web.
|
||||
|
||||
## [2026-06-18] ingest | Device-status footer — unified monitor across all categories
|
||||
|
||||
Generalised printer-only status monitoring to a booth-wide DEVICE-STATUS FOOTER covering relays/readers/cameras/printers. New `DeviceMonitor` (`apps/server/src/device-monitor.ts`) polls every enabled device each tick (default 8s): printers via rich `readStatus()`, all others via the generic `healthCheck()` reachability probe, flattened to one traffic-light (ready/degraded/offline)+detail, deduped (emits on change only), fail-toward-offline (a throw/timeout → offline, never false-healthy). New `device-status` bus event + `GET /api/devices/status` snapshot; live updates ride the existing `/api/ws` (`hello` now carries the initial device set; `device-status` frame per change). Web: live-store `devices` map (setDevices/upsertDevice), WS handler wired, new `DeviceFooter` chip-per-device with an "all ready / N offline" roll-up, mounted in the app shell; `devices` i18n namespace (sq/en). The PrinterMonitor + its SSE stream stay as the printer-specific authority (both run — see the note in [[device-status-monitoring]]). Filed [[device-status-monitoring]] (resolves the code link), cross-linked [[printer-status-monitoring]] + [[booth-console]], indexed (concepts 41→42). Verified on a fresh DB (relay+reader → ready via healthCheck; unreachable printer → offline with detail, no throw; emit-once-then-silent) — 9/9; server+web build clean.
|
||||
|
||||
## [2026-06-18] refine | Device footer — role-only labels + click-to-see-issues
|
||||
|
||||
Two refinements to the device-status footer. (1) Chips label by ROLE, not vendor: the server sends a structured `roleKind` token per device (reader/camera → direction inherited from the bound relay via `directionOf()`; access → entry/exit/both, or "mixed" across relays; printer → lane/booth) and the client localises category+role → "Lexuesi hyrje", "Printer kabina", "Kamera dalje". Dropped `label`/`role`/driverId from the chip. (2) Fault detail no longer pollutes the footer: chips are compact (dot + label); a degraded/offline chip (or the "N with issues" roll-up) is clickable and opens a small issues panel above the footer listing only the problem devices with state/detail/checked-time (outside-click/Esc to close; no new dependency). i18n `devices.role.*` + issues keys (sq/en). Verified roleKind resolution on a fresh DB (access→mixed, reader(exit)→exit, camera(entry)→entry, printers→lane/booth) 7/7; server+web build clean. Updated [[device-status-monitoring]].
|
||||
|
||||
## [2026-06-18] fix | Stuck active session — paid ticket that never got a vehicle_exit (T-397815c0)
|
||||
|
||||
Investigated a paid ticket stuck forever in the Active Sessions tab. Root cause (confirmed from the live ledger): the car left via a **manual barrier re-open**, which by design signed an `anomaly` but **never a `vehicle_exit`** — so `activeSessions()` saw it as permanently `open` (the grace-expiry eviction only applied to *exited* sessions). The normal exit that would have signed the exit was refused because walk-back grace (5 min) had expired ~17h earlier. Two fixes: (1) `ExitFlow.reopenBarrier` now signs a `vehicle_exit` (`source:manual`) **when the session is still open**, closing it — while still NOT double-signing an already-exited session (phantom re-close). (2) `PayStation.activeSessions()` ages out a **paid** open session past grace even with no exit (unpaid open sessions never age out — a car owing money stays). Plus a one-off corrective: appended a signed `vehicle_exit` (index 68, `correction:true`) for T-397815c0 through EventLog (chain verified `{ok:true}`), clearing it from the list. Verified both fixes on a fresh DB (9/9; chain intact). Updated [[booth-exit-flow]] (active-session definition + the re-open rule, was "NEVER a vehicle_exit").
|
||||
|
||||
## [2026-06-18] ingest | Permit → Subscription rename + monthly pricing (timeframes deferred)
|
||||
|
||||
Renamed the "permit" feature to "subscription" (operator term: abonim) and added recurring monthly pricing. FULL rename of mutable master data: tables permits→subscriptions, permit_credentials→subscription_credentials, permit_plates→subscription_plates, sessions.permit_id→subscription_id (data-preserving ALTER RENAMEs, migration 0004); server permit-flow.ts→subscription-flow.ts (SubscriptionFlow), routes/permits.ts→routes/subscriptions.ts (/api/subscriptions), web PermitManager→SubscriptionManager, api types, i18n (sq "Abonimet"/en "Subscriptions"). The signed ledger `permitId` payload field is INTENTIONALLY kept (immutable hash-chained history — renaming would break verification of past events); code/data are "subscription", the on-chain field stays `permitId`. Pricing: per-subscription priceMinor + period("monthly") + currency, with a site default (site_config.subscription_monthly_price_minor) pre-filling the form; collecting the fee into the ledger/shift is DEFERRED (wiki note only). Time-of-day access windows (e.g. overnight subscriber 19:00–07:00, transient outside) documented as a design note in [[subscription]] — NOT implemented; legacy precedent in [[parksql2017-legacy-schema]] (MembershipPlansTime). Renamed [[entities/permit|permit]]→[[subscription]] and swept all [[permit]] wikilinks across the wiki (log.md historical entries left as-was). Verified end-to-end on a fresh migrated DB (schema+pricing, card entry/exit, maxConcurrent cap, on-chain permitId carries the sub id, chain verify) 6/6; migration also applied cleanly to a copy of the live DB (18 sessions preserved). Full monorepo builds clean.
|
||||
|
||||
## [2026-06-18] note | Subscription-fee collection is a SHIFT transaction
|
||||
|
||||
Clarified (user): collecting/renewing a subscription's monthly fee is a financial transaction a common operator makes DURING their shift — it must reflect in THAT shift's drawer + Z-report, not be an admin-only edit. Updated [[subscription]] (Pricing → "Collecting the fee is a SHIFT transaction"): model it as a signed `payment` event (same `{amountMinor,currency,tender}` shape) tagged `{subscriptionId}` at collection time, so it folds into the open shift automatically (Z-report sums payments by time; drawer adds cash tenders) with no new summing logic. Admin edits the master data; operator takes the money. Subscription entry/exit stay free — only the plan fee is a payment. Still DEFERRED build; cross-linked from [[shift]] ("What End Shift does"). Open: plain `payment`+tag vs. a distinct `subscription_payment` type (leaning plain).
|
||||
|
||||
Reference in New Issue
Block a user