feat(subscription): RFID enrollment, any-credential exit, prepaid booth handling
Rounds out subscriptions across enrollment, the barrier flow, and the booth.
- RFID credentials enabled with a "Read card" enrollment flow: the operator
arms ONE chosen reader (CredentialCapture, single-shot + ~30s TTL); that
reader's next read is captured into the form and NOT dispatched to the access
flow — the OTHER reader keeps serving live entry/exit. Routes:
/api/subscriptions/readers + /capture/{arm,cancel} + poll.
- Enter with one credential, exit with another: sessions are keyed by a
per-occurrence id (SUBSESS-<short>), not the credential value, with
permitId in the payload. Direction is decided by the barrier the reader sits
at (entry-lane→entry, exit-lane→exit; "both" infers); a fleet (maxConcurrent>1)
admits several cars and exits any with any credential, FIFO (oldest first).
- Booth treats a subscription occurrence as PREPAID: never quoted/charged; the
pay/exit modal shows a subscription mode (snapshots + a single audited
Open-barrier action) to assist a faulty exit reader / missing card;
reopenBarrier authorizes paidAt!=null OR subscription. Active Sessions badges
"abonim" and labels by holder name (not the raw key).
- Plus a per-read diagnostic log in the QR-reader route (serial → device →
verdict/dir), which surfaced the earlier duplicate-reader-IP misroute.
Verified via buildServer+inject + reader-scan/TCP-capture simulations
(enrollment isolation, cross-credential + FIFO fleet, prepaid-not-charged,
subscription reopen, unpaid-transient guard). Updated wiki (subscription,
booth-exit-flow). No migration.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -95,12 +95,23 @@ For an active session, the operator can open the barrier as a **human interventi
|
||||
> case. The [[#a-session-is-active|paid age-out]] above is the belt-and-braces safety net for any
|
||||
> paid session that still slips through.
|
||||
|
||||
**Guard — no payment, no button.** The "Open barrier" action is shown/active **only for sessions that
|
||||
have a payment** (paid, or paid-and-exited-in-grace). An **unpaid** open session has **no barrier-open
|
||||
affordance at all** — the row routes to the [[#operator-flow|pay/exit modal]] instead. The
|
||||
no-unpaid-bypass rule is enforced structurally: the button simply does not exist for an unpaid car.
|
||||
(A future reason-required *force exit* for genuine disputes would be a separately-audited path — see
|
||||
Open.)
|
||||
**Guard — paid OR subscription, else no button.** The "Open barrier" action is shown/active for a
|
||||
session that **has a payment** (paid, or paid-and-exited-in-grace) **OR is a [[subscription]]
|
||||
occurrence** (prepaid — the operator must be able to assist a subscriber when the exit reader / card
|
||||
fails). An **unpaid TRANSIENT** open session has **no barrier-open affordance** — the row routes to
|
||||
the [[#operator-flow|pay/exit modal]] instead. The no-unpaid-bypass rule is enforced structurally
|
||||
(server-side in `reopenBarrier`: `paidAt != null || subscription`). A future reason-required *force
|
||||
exit* for genuine disputes would be a separately-audited path — see Open.
|
||||
|
||||
### Subscription occurrences in the booth (built 2026-06-18)
|
||||
|
||||
A subscriber's car shows in Active Sessions as a **subscription** session (badge "abonim"; labelled by
|
||||
the **holder name**, not the raw `SUBSESS-…` key). Opening it shows the **pay/exit modal in
|
||||
subscription mode**: entry/duration + **PREPAID** (no amount — it is **never quoted or charged**),
|
||||
the entry/exit **snapshots**, and a single **Open barrier** action (the audited re-pulse). This is
|
||||
exactly the assist path for a **faulty exit reader or a missing/forgotten card/QR**. The session
|
||||
view (`lookup` / `activeSessions` in `pay-station.ts`) carries `subscription`, `subscriptionId`,
|
||||
`subscriptionHolder`, derived from the entry payload's `permit:true` / `permitId`.
|
||||
|
||||
This single mechanism covers both edge cases: a **damaged ticket / dead scanner** (find the still-open
|
||||
session in the list → pay/exit modal, or if already paid → Open barrier, no scan needed), and a
|
||||
|
||||
Reference in New Issue
Block a user