docs(wiki): subscription recurring billing + party ledger — design only

Subscription page: the one-window model can't express prepaid/postpaid, calendar or
anniversary anchoring, grace or an expiry notice; and renewal is OFF-BOOK today (a PUT
that appends no payment — the same hole closed for the first sale on 2026-06-20).
Designed: plan billing rule, per-day pricing so both anchors share one formula, open-
ended agreement, subscription_periods where each period is a ledger charge and a renewal
= paying the next period, one subscriptionAccess() gate function, expiry notice derived
not stored.

New decision page party-ledger: a counterparty sub-ledger for who-owes-whom across
modules — parties + signed charge / settlement / write_off events, balance derived never
stored, aging + statements + CSV; lands postpaid subscriptions, hotel guest-nights,
fleet washes on account, supplier/utility bills. Sub-ledger only: no bookkeeping, a
statement is not a fiscal invoice, parties per appliance. validation-sponsorship's
sponsor table marked superseded; open-questions #17; index.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-09 10:19:09 +02:00
parent 535244209a
commit 4fd175e0e4
5 changed files with 290 additions and 10 deletions
+9 -1
View File
@@ -2,12 +2,20 @@
type: concept type: concept
tags: [parking, domain, business, pricing, validation, design] tags: [parking, domain, business, pricing, validation, design]
sources: [parksql2017-legacy-schema] sources: [parksql2017-legacy-schema]
updated: 2026-06-17 updated: 2026-09-08
status: open status: open
--- ---
# Validation & Sponsorship — merchant comps, coupons, postpaid B2B # Validation & Sponsorship — merchant comps, coupons, postpaid B2B
> **Sponsor accounts superseded (2026-09-08).** The `sponsors` table sketched below — a
> counterparty with a stored `balance_minor` and a billing period — is now a special case of the
> **[[party-ledger]]** (design): any party (subscriber, hotel, fleet, supplier) with a balance
> *derived* from signed `charge` / settlement / `write_off` events, never a stored column. A
> postpaid sponsor = a party; each comped stay = a `charge` against it; the monthly invoice = its
> statement. The validation *mechanics* (signed validation events on a session) are unchanged
> and built ([[validation-discounts]]).
Builds on [[validation-discounts]] (the signed-event discount mechanism) to add the layer it leaves Builds on [[validation-discounts]] (the signed-event discount mechanism) to add the layer it leaves
open: **a sponsor account and postpaid B2B billing.** The driving case — **a nearby business with a open: **a sponsor account and postpaid B2B billing.** The driving case — **a nearby business with a
postpaid agreement whose customers enter and exit freely, billed to the business monthly.** postpaid agreement whose customers enter and exit freely, billed to the business monthly.**
+9 -1
View File
@@ -2,7 +2,7 @@
type: decision type: decision
tags: [parking, decisions, open] tags: [parking, decisions, open]
sources: [parking-system-architecture] sources: [parking-system-architecture]
updated: 2026-09-04 updated: 2026-09-08
status: open status: open
--- ---
@@ -148,3 +148,11 @@ procurement. (See [[parking-system-architecture]] §10.)
already carried `subscription:*` (stale note); roles now remember the jobs they follow and already carried `subscription:*` (stale note); roles now remember the jobs they follow and
a grown job is re-applied with one click, never silently; every role edit is signed as a a grown job is re-applied with one click, never silently; every role edit is signed as a
`config_change`. **Settled** — details on [[venue-modules]] §"Permissions matrix" Status. `config_change`. **Settled** — details on [[venue-modules]] §"Permissions matrix" Status.
17. **Party ledger — receivables & payables across modules.** _(Raised by the user, 2026-09-08.)_
Postpaid [[subscription]]s, hotel guest-nights billed to the hotel, Car Wash fleet deals on
account, and supplier/utility bills all need "who owes whom". Designed as a **counterparty
sub-ledger** — parties + signed `charge` / settlement / `write_off` events, balance derived,
aging + statements, CSV for the accountant — see [[party-ledger]] (design only, not built).
Interacts with #9 (a statement is **not** a fiscal invoice; fiscalisation is off-appliance)
and #8 (one currency per party until FX exists). Also reopened on the subscription page: a
**renewal is currently off-book** (an edit, no `payment`).
+149
View File
@@ -0,0 +1,149 @@
---
type: decision
tags: [parking, decisions, open, finance, ledger, modules, subscriptions, carwash]
sources: []
updated: 2026-09-08
status: open
---
# Party ledger — who owes the site, and whom the site owes
**Design only (2026-09-08). Nothing built.** Captured from a design conversation with the user; to be
refined before any code. The trigger was the [[subscription]] billing redesign: as soon as a
subscription can be **postpaid**, the site is *collecting a debt*, and the user immediately listed
three more debtors/creditors that need the same treatment. So this is not a subscription feature
— it is a **counterparty sub-ledger** that subscriptions, hotels, fleets and suppliers all sit on.
## The problem stated
The user's constraints, verbatim in spirit:
1. **Postpaid subscriptions** — the subscriber pays at the start or end of a month; the site must
see what is unpaid.
2. **Hotels** — occasional daily access for a hotel's guests, billed to the hotel, not the guest.
3. **Car Wash fleet deals** — the wash cleans a company's cars; payment is due per period; the
*site* collects the debt.
4. **Car Wash suppliers and utility bills** — the wash needs to see what it has paid and still owes
its suppliers (detergent, water, electricity).
5. **The admin needs one view of uncollected dues: who owes what to the park.**
Today none of this is modelled. Money exists in exactly two shapes: a signed `payment` at a till
([[shift]], [[append-only-event-chain]]) and a drawer voucher (`cash_in` / `cash_out`). Neither
names a *counterparty*, so "who owes whom" cannot be asked. The earlier
[[validation-sponsorship]] page sketched a `sponsors` table with a stored `balance_minor` for the
postpaid-merchant case; this page **supersedes that sketch** with something general.
## The decision (proposed)
Add **one core concept, once**: a **party** with an **account**, and three signed ledger event
types that move that account. Modules (Parking, Car Wash, later Bar — [[venue-modules]]) append
charges against parties; the core owns the party master data, the balance derivation, the
statement and the aging report. No module keeps its own receivable.
### Party (core master data, module-agnostic)
A party is any legal or natural person the site has money dealings with — a subscriber, a hotel, a
fleet company, a utility, a supplier. Mutable master data (like `subscriptions`), soft-deletable
([[soft-delete]]):
```
parties id, name, contact, taxId?, currency, kind {customer|supplier|both},
creditLimitMinor?, terms {dueDays | calendarDay}, active, deletedAt…
```
A subscriber gets a party row (created with the subscription, or linked to an existing one — a
company with five subscriptions is one party). `creditLimitMinor` lets a desk **refuse on-account
sales** when the party is over its limit; `terms` gives the default due date of a charge.
### Three signed event types (the account never stores a balance)
| Event | Meaning | Payload (signed) | Who appends |
| --- | --- | --- | --- |
| `charge` | an **accrual** — the party now owes (or is owed) | `partyId, direction {receivable\|payable}, amountMinor, currency, source {module, ref}, periodFrom?, periodTo?, dueAt, operator` | a module (subscription period, guest-night, on-account wash, supplier bill) |
| `settlement` | **money moved** against the account | as a **`payment`** at a till (`partyId` + `chargeIds[]` added) for cash/card received; a **`cash_out`** voucher with `partyId` for cash paid out; a `settlement` with `tender: "bank"` and no till for transfers either way | operator at a till / admin for bank |
| `write_off` | admin-signed **reduction with a reason** (waived period, disputed night, goodwill) | `partyId, chargeId, amountMinor, reason, operator` | admin only |
**Balance** per party and currency = Σ charges − Σ settlements − Σ write-offs, derived on read
(cached at most), never stored. **Why signed events and not a mutable `balance` column:** the
[[threat-model]] adversary is the booth/wash operator. A receivable that lives in a mutable row can
be quietly shrunk; a receivable that is a chain of signed events cannot — a statement is
re-derivable and **disputable against the chain**, the same guarantee the shift Z-report gives.
The one fraud-relevant path is the write-off, which is why it is admin-gated and permanent.
Reusing `payment` for money received (rather than inventing a parallel type) keeps the drawer,
the Z-report and the per-till folds ([[shift]] §Tills) working with **zero new summing surface** —
the same reasoning that made a subscription sale a `payment` with a `subscriptionSale` flag
([[subscription]] §Collecting the fee).
### How the four cases land on it
- **Subscriptions** — the billing-period design ([[subscription]] §Recurring billing) stays exactly
as drawn, except a billing period *is* a `charge` against the subscriber's party. Prepaid vs
postpaid is only the due-date rule. Paying a period = a till `payment` referencing the charge.
- **Hotels** — a subscription-like agreement whose **payer is the hotel party**, postpaid, whose
credential is issued per guest for N nights (the existing `"day"` plan). Each guest-night is a
charge line; the hotel receives a monthly **statement of nights**. The guest never pays.
- **Fleet washes** — the wash order gains a **third `payAt` beside `booth` and `bay`: `account`**.
The order is a charge against the fleet party; the wash till's Z-report shows on-account sales
as a separate line, *not* cash. Over the credit limit → the wash desk cannot pick `account`.
- **Suppliers and utilities** — a bill is a **payable** charge against that party (the wash's
detergent supplier, the electricity company). Paying it from the wash till is a `cash_out`
voucher that references the bill (the drawer already folds it); paying by bank is a bank
settlement. The owner sees what is owed, what was paid, and **from which till**.
### The admin view
One report over all parties: name, balance, oldest unpaid charge, **aging buckets** (current,
30, 60, 90+ days), drill-down to a **statement** for a period (every charge, settlement and
write-off, each linked to its signed event). "Uncollected dues" is a filter on it: receivables
with a balance. Payables are the same report with the direction flipped. Everything is a
projection over the ledger, like [[reporting-analytics]].
### Permissions
New core permissions, in the [[venue-modules]] matrix: `finance:read` (statements, aging),
`finance:settle` (record a bank settlement; till settlements ride the existing pay permissions),
`finance:writeoff` (admin), `party:manage` (master data). The wash desk sees only *whether* a
party is on-account-eligible, never the balance.
## Where the line is drawn
This is a **sub-ledger of receivables and payables, not bookkeeping.** No chart of accounts, no
profit-and-loss, no VAT computation, no double-entry general ledger. The accountant gets a **CSV
export** of charges and settlements per party and period. Two flags before anything is built:
- **A statement is not a fiscal invoice.** Fiscal receipts/invoices are already
[[open-questions]] #9 (tax number, sequential numbering, and — in Albania — fiscalisation).
The appliance is [[offline-first]]; fiscal invoicing needs the cloud side
([[cloud-service-saas]]) or an external fiscal device. Statements must be **labelled as
statements** so nobody mistakes them for invoices.
- **Parties are per appliance.** A fleet washing at two sites has two accounts until the
PostgreSQL sync target exists. Consolidation is a cloud-side concern.
Also deliberately **not** built: automatic card charging, dunning sequences, automatic
suspension without a grace period. The operator never types a price ([[subscription]] rule).
## Build order (each step usable on its own)
1. `parties` + the three event types + the balance/aging/statement report and CSV export.
2. Subscription billing periods on top ([[subscription]] §Recurring billing) — the renewal
off-book hole closes here.
3. `payAt: "account"` on Car Wash orders, with the credit-limit gate and the Z-report line.
4. Bills and payables (supplier / utility register; `cash_out` with a bill reference).
## Open
- Does a **guest-night** charge get appended at credential issue (N nights known up front) or per
actual entry? Issue-time matches the hotel's booking; per-entry matches reality. Lean issue-time,
with a void path if the guest never came.
- **Currency**: parties carry one currency; a charge in another is refused until the FX question
([[open-questions]] #8) is settled.
- **Who may create a party** at the wash desk vs. admin only (a fleet deal is a contract, not a
walk-in).
- Should utility bills live in this app at all, or only supplier bills paid from a till? The user
asked for both; the register is cheap, the temptation to grow it into bookkeeping is the risk.
- **Reminders to the party** (statement by email/SMS) are off-appliance — same answer as the
subscription expiry notice: the operator/owner is notified, the contact is theirs to make.
Related: [[subscription]] · [[validation-sponsorship]] (superseded sketch) · [[venue-modules]] ·
[[shift]] · [[append-only-event-chain]] · [[threat-model]] · [[reporting-analytics]]
+120 -6
View File
@@ -2,7 +2,7 @@
type: entity type: entity
tags: [parking, domain, business, subscriptions, identity, pricing] tags: [parking, domain, business, subscriptions, identity, pricing]
sources: [] sources: []
updated: 2026-06-20 updated: 2026-09-08
aliases: [subscription-plan] aliases: [subscription-plan]
status: open status: open
--- ---
@@ -90,6 +90,15 @@ subscription row, one window. The amount the operator should collect is **N × t
`now` ∈ [validFrom, validTo]** — so a 3-month window simply stays valid for three months. `now` ∈ [validFrom, validTo]** — so a 3-month window simply stays valid for three months.
- An explicit **`validTo` override** is still accepted (manual end date) when `months` isn't used. - An explicit **`validTo` override** is still accepted (manual end date) when `months` isn't used.
> ⚠ **Renewal is OFF-BOOK (found 2026-09-08).** "Renewing is just editing the window" means a
> renewal goes through `PUT /api/subscriptions/:id`, which by design **never re-sells and appends
> nothing to the ledger**. The first sale was put on the chain on 2026-06-20 precisely because
> 27,000 ALL had gone off-book; **every renewal since takes the same off-book path** — the
> operator collects the next month's fee and moves `validTo`, with no `payment` event. The
> recurring-billing design below closes this: a renewal becomes *paying the next billing period*,
> a signed `payment`. Until then, a renewal should be taken as a **new sale** (new subscription
> row), not an edit.
### v2 — quantity, plan timeframes (tariff bridge), reserved spots (built 2026-06-20) ### v2 — quantity, plan timeframes (tariff bridge), reserved spots (built 2026-06-20)
Three enhancements driven by real scenarios (migration `0011`): Three enhancements driven by real scenarios (migration `0011`):
@@ -356,6 +365,106 @@ Intended behaviour (to design + build later):
> **Explicitly postponed.** For now this is documentation only — no schema, no enforcement. A > **Explicitly postponed.** For now this is documentation only — no schema, no enforcement. A
> subscription is valid whenever it is active and within `validFrom`/`validTo`, all day. > subscription is valid whenever it is active and within `validFrom`/`validTo`, all day.
## Recurring billing — prepaid / postpaid, calendar or anniversary — DESIGN 2026-09-08
**Design only, nothing built.** Captured from a design conversation with the user (2026-09-08):
"a subscriber should prepay or postpay every month, on the 1st or on the day the subscription
began; a subscription fixed by a daily tariff, e.g. 300 ALL/day; for prepaid, a notice that a
subscription is about to expire so the owner/operator warns the subscriber to pay or lose access."
The financial side of this grew into its own page — the [[party-ledger]] — because a postpaid
subscriber is a *debtor*, and the site has other debtors (hotels, fleets) and creditors
(suppliers). This section is the subscription-shaped part.
### What is wrong with the one-window model
A subscription today is **one coverage window** (`validFrom`/`validTo`) sold once: a hotel model.
There is no recurring agreement, no due date, no grace, no unpaid balance; prepaid vs postpaid is
not expressible, and calendar-anchored billing can only be faked with hand-picked dates. And
renewal is off-book (callout above).
### Split the one row into three concepts
**1. Plan** — the catalog and versioning stay; a plan version gains a **billing rule**:
```
billing: {
mode: "prepaid" | "postpaid",
cycle: "day" | "week" | "month", // how often a period is billed
anchor: "calendar" | "start", // the 1st of the month, or the sale's anniversary
graceDays: number, // access continues this long past due
noticeDays: number // "about to expire" window
}
```
Recurring plans are **priced per day** (`period: "day"`): a calendar month costs
`daysInMonth × 300 ALL`, a partial first month is simply the days left, and **calendar and
anniversary anchoring share one formula** (proration falls out for free). Fixed-price monthly
plans (`period: "month"`) stay for sites that want a flat number. The hotel "N nights" sale is
unchanged (a `"day"` plan over a span, no billing rule).
**2. Agreement** — the `subscriptions` row: holder, credentials, cars, `validFrom`; for a
recurring plan **no `validTo`** (open-ended, ends by revoke/suspend). Fixed spans keep `validTo`.
The holder is (or is linked to) a **party** ([[party-ledger]]) — the payer, which for a hotel is
the hotel, not the guest.
**3. Billing periods** — one row per cycle, and each is a **`charge`** on the party ledger:
```
subscription_periods id, subscriptionId, periodFrom, periodTo,
amountMinor (from the plan version), currency, dueAt,
status {due|paid|overdue|waived}, chargeEventId, paymentEventId?
```
- **Paying a period** = the existing signed **`payment`** with `subscriptionSale: true` plus the
period/charge reference — drawer and Z-report keep working with no new summing
(§Collecting the fee). **Renewal is just paying the next period.** This closes the off-book hole.
- **Waiving** a period is a signed **$0 payment with a reason** — the same rule the Car Wash uses
for a comp ([[venue-modules]]: a comp never opens the barrier, sign the $0 payment) — or a
`write_off` on the party ledger; admin-gated either way.
- The next period is **generated ahead** (prepaid: before the current one ends, so it can be paid
early; postpaid: at period end, due `dueAt`), by a daily tick or lazily on read.
### The gate asks one function
The entry flow stops reading `validTo` for recurring plans and asks
`subscriptionAccess(sub, periods, now) → { ok, reason, accessUntil, daysLeft }`:
- **prepaid** — allowed while `now ≤ paidThrough + graceDays` (the next period must be paid
before it starts, plus grace);
- **postpaid** — allowed while no period is unpaid past `dueAt + graceDays`;
- both collapse to one derived **`accessUntil`** and **`daysLeft`** per subscriber (never stored).
This also answers the long-open **lapsed-mid-stay** question for recurring subs: a period ending
while a car is parked falls into **grace**, so nobody is trapped; only a subscriber still parked
past grace becomes a transient at exit (the tariff-bridge machinery above already prices that).
Revoked/suspended behaviour is unchanged.
### "About to expire" — derived, not stored
One endpoint (e.g. `GET /api/subscriptions/attention`) lists subscribers whose `accessUntil` falls
within the plan's `noticeDays`, those in grace, and those overdue. Surfaced in three places:
1. a **counter on the booth console** ([[booth-console]]);
2. a **badge in the subscriber list**;
3. a **line in the live feed when such a subscriber scans in** — "expires in 3 days" at the moment
the person is at the gate (a slip can print, best-effort like the window-charge notice).
Contacting the subscriber stays with the operator/owner by phone (`contact` field). SMS/email
is off-appliance ([[cloud-service-saas]]) — a separate decision.
### Not built, deliberately
Automatic card charging, invoices, dunning, auto-suspension without grace. **The operator still
never types a price.**
### Build order (after [[party-ledger]] step 1)
1. Billing rule on the plan version + `subscription_periods` (migration); period generation.
2. Pay-period route (signed `payment` + charge reference) and the `subscriptionAccess` gate
function in `subscription-flow.ts`; `PUT` stops moving `validTo` on recurring subs.
3. Attention endpoint + the three UI surfaces.
4. Wiki + [[booth-console]] docs.
## Data model (as-built 2026-06-18) ## Data model (as-built 2026-06-18)
Tables (mutable master data; every *use* still produces a signed `vehicle_entry`/`vehicle_exit`): Tables (mutable master data; every *use* still produces a signed `vehicle_entry`/`vehicle_exit`):
@@ -420,10 +529,15 @@ subscription** (card/QR credential, or a bound plate) — otherwise to the trans
1. **Reader hardware** — confirm the RF reader and QR/optical reader models (procurement; [[bom]], 1. **Reader hardware** — confirm the RF reader and QR/optical reader models (procurement; [[bom]],
[[open-questions]]). [[open-questions]]).
2. **Lapsed-mid-stay & revoked** policy (fall back to transient [[tariff]] vs. refuse) — confirm. 2. **Lapsed-mid-stay & revoked** policy (fall back to transient [[tariff]] vs. refuse) — confirm.
3. ~~**Subscription-fee collection**~~ — **RESOLVED + BUILT 2026-06-20.** Selling a priced 3. ~~**Subscription-fee collection**~~ — **RESOLVED + BUILT 2026-06-20** for the *first* sale
subscription appends a signed `payment` (`subscriptionSale` flag, `priceMinor × months`, (signed `payment`, `subscriptionSale` flag, operator-chosen tender, folds into the
operator-chosen tender) that folds into the drawer/Z-report. Remaining sub-question: should a sale drawer/Z-report). **REOPENED 2026-09-08 for RENEWALS**: a renewal is a `PUT` that appends
be **hard-blocked without an open shift** (it isn't today — it warns instead)? See "Collecting the nothing (see the callout under "Multi-month"). Closed by the recurring-billing design (a renewal
fee". = paying the next period). Remaining sub-question: should a sale be **hard-blocked without an
open shift** (it isn't today — it warns instead)?
4. **Time-of-day access windows** (overnight subscribers) — design + build; boundary-case policy 4. **Time-of-day access windows** (overnight subscribers) — design + build; boundary-case policy
above (see the design note). above (see the design note).
5. **Recurring billing** (prepaid/postpaid, calendar/anniversary anchor, grace, expiry notice) —
**designed 2026-09-08, not built**; see §Recurring billing and [[party-ledger]]. To refine: is
the next period generated by a daily tick or lazily; does a waived period sign a $0 `payment` or
a `write_off` (pick one); whether `noticeDays` is per plan or per site.
+3 -2
View File
@@ -102,7 +102,7 @@ Counts: 4 sources · 19 entities · 47 concepts · 8 decision records.
- [[site-metadata]] — optional park identity (name, operator, VAT, address, contact) in site_config; feeds the ticket header. - [[site-metadata]] — optional park identity (name, operator, VAT, address, contact) in site_config; feeds the ticket header.
- [[valet-overcapacity]] — "full" is soft: operator may valet-accept over capacity (keys handed over, custody). Manned, deferred. - [[valet-overcapacity]] — "full" is soft: operator may valet-accept over capacity (keys handed over, custody). Manned, deferred.
- [[validation-discounts]] — BUILT (2026-07-13): in-park merchant (bar/lavazh) users scan-and-validate on their device (signed event, program↔user binding); booth settles NET + prints gross/discount/net; comp/time-credit/fixed/percent, caps, /setup/site panel, /validate screen. - [[validation-discounts]] — BUILT (2026-07-13): in-park merchant (bar/lavazh) users scan-and-validate on their device (signed event, program↔user binding); booth settles NET + prints gross/discount/net; comp/time-credit/fixed/percent, caps, /setup/site panel, /validate screen.
- [[validation-sponsorship]] — design: sponsor accounts + postpaid B2B (customers park free, business billed monthly); not a permit. - [[validation-sponsorship]] — design: postpaid B2B sponsorship (customers park free, business billed monthly); its sponsor-account sketch is superseded by [[party-ledger]].
- [[reporting-analytics]] — revenue/occupancy/stay reports + plate-search, all projections over the signed log. - [[reporting-analytics]] — revenue/occupancy/stay reports + plate-search, all projections over the signed log.
- [[clock-integrity]] — fees depend on the host clock; detect/flag backdating on an offline box. - [[clock-integrity]] — fees depend on the host clock; detect/flag backdating on an offline box.
- [[ticket-encoding]] — transient ticket id (11-digit numeric + Luhn) as Code128; printed at entry, scanned at pay station + exit; barcode geometry must fit paper width (KP-300H overflow); plate-as-ticket alt. - [[ticket-encoding]] — transient ticket id (11-digit numeric + Luhn) as Code128; printed at entry, scanned at pay station + exit; barcode geometry must fit paper width (KP-300H overflow); plate-as-ticket alt.
@@ -110,7 +110,7 @@ Counts: 4 sources · 19 entities · 47 concepts · 8 decision records.
- [[device-events]] — unsigned hardware telemetry (relay/printer/camera/reader/input); separate from the signed ledger. - [[device-events]] — unsigned hardware telemetry (relay/printer/camera/reader/input); separate from the signed ledger.
- [[app-logs]] — the third stream: diagnostic logs (backend warn+ pino sink + frontend errors) → app_logs; log:read viewer; pruned by age+row cap. - [[app-logs]] — the third stream: diagnostic logs (backend warn+ pino sink + frontend errors) → app_logs; log:read viewer; pruned by age+row cap.
- [[soft-delete]] — BUILT: accidental admin deletes of master data (users/roles/subs/plans/tariffs) are soft (deleted_at) + recoverable from a recycle bin; auto-purge after N days; signed ledger out of scope. - [[soft-delete]] — BUILT: accidental admin deletes of master data (users/roles/subs/plans/tariffs) are soft (deleted_at) + recoverable from a recycle bin; auto-purge after N days; signed ledger out of scope.
- [[subscription]] — recurring plan (e.g. 10,000 ALL/month); RF/QR or plate identity, car-count + max-concurrent, host-in-loop; short-circuits payment. (Renamed from "permit"; time-of-day windows noted, deferred.) - [[subscription]] — recurring plan (e.g. 10,000 ALL/month); RF/QR or plate identity, car-count + max-concurrent, host-in-loop; short-circuits payment. Plan catalog + tariff bridge built. 2026-09-08: **renewal found off-book**; recurring billing (prepaid/postpaid, calendar/anniversary, grace, expiry notice, billing periods as ledger charges) designed, not built.
- [[opencv-anpr-service]] — host-side vision microservice: ANPR (plate identity) + vehicle verification (anti-plate-spoofing witness); fast-alpr (MIT, YOLOv9+CCT/ONNX) the evaluated recognizer baseline. - [[opencv-anpr-service]] — host-side vision microservice: ANPR (plate identity) + vehicle verification (anti-plate-spoofing witness); fast-alpr (MIT, YOLOv9+CCT/ONNX) the evaluated recognizer baseline.
- [[lane-presence-and-anpr-entry]] — camera vehicle detection → (BUILT) advisory lane busy/free booth lights + (BUILT) the ANPR "bridge" (`anpr-entry.ts`): a subscriber's plate read at the lane admits them via the existing gated subscription flow (match-before-emit; subscriber-only). Measured camera limits; rejected the queue-tracking/livestream ideas. - [[lane-presence-and-anpr-entry]] — camera vehicle detection → (BUILT) advisory lane busy/free booth lights + (BUILT) the ANPR "bridge" (`anpr-entry.ts`): a subscriber's plate read at the lane admits them via the existing gated subscription flow (match-before-emit; subscriber-only). Measured camera limits; rejected the queue-tracking/livestream ideas.
- [[vision-service-hardening]] — fix/hardening backlog for `apps/vision/` (2026-07-02 reviews): DoS (body-cap, pixel-bomb, event-loop-blocking inference), unauthenticated + operator-writable model weights, `0.0.0.0` default bind, + correctness/hygiene items. Not yet fixed — the to-do list. - [[vision-service-hardening]] — fix/hardening backlog for `apps/vision/` (2026-07-02 reviews): DoS (body-cap, pixel-bomb, event-loop-blocking inference), unauthenticated + operator-writable model weights, `0.0.0.0` default bind, + correctness/hygiene items. Not yet fixed — the to-do list.
@@ -137,6 +137,7 @@ Counts: 4 sources · 19 entities · 47 concepts · 8 decision records.
- [[vision-service-packaging]] — the vision service lives in this monorepo (apps/vision/), separate process, wired into Turbo via a package.json shim; uv-managed Python. - [[vision-service-packaging]] — the vision service lives in this monorepo (apps/vision/), separate process, wired into Turbo via a package.json shim; uv-managed Python.
- [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events). - [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events).
- [[desktop-shell-tauri]] — ✅ Tauri v2 chosen over Electron for the desktop kiosk shell; thin wrapper, server keeps all logic. Best case Ubuntu 26.04 LTS (resolves WebKitGTK); worst case Windows+WSL → kiosk browser, no native shell. Auto-updater mirrors signed releases to public `mca/public_releases` (source repo is private — field appliances have no Gitea creds). - [[desktop-shell-tauri]] — ✅ Tauri v2 chosen over Electron for the desktop kiosk shell; thin wrapper, server keeps all logic. Best case Ubuntu 26.04 LTS (resolves WebKitGTK); worst case Windows+WSL → kiosk browser, no native shell. Auto-updater mirrors signed releases to public `mca/public_releases` (source repo is private — field appliances have no Gitea creds).
- [[party-ledger]] — 🟡 DESIGN (2026-09-08, not built): counterparty sub-ledger for who-owes-whom across modules — parties + signed `charge` / settlement / `write_off` events, balance derived never stored, aging + statements + CSV; lands postpaid subscriptions, hotel guest-nights, fleet washes on account, supplier/utility bills. Sub-ledger only: no bookkeeping, statements are not fiscal invoices, parties per appliance.
- [[venue-modules]] — 🟡 OPEN: optional per-site modules (Car Wash, Bar/Restaurant) with Parking as a peer module on a venue POS/audit core; manifest registry, entitled ∩ activated enablement (vendor env + site-admin config), validation kept for the Bar (Lavazh station retires with Car Wash), name stays parking-system, vision vehicle-category as an advisory anomaly flag. - [[venue-modules]] — 🟡 OPEN: optional per-site modules (Car Wash, Bar/Restaurant) with Parking as a peer module on a venue POS/audit core; manifest registry, entitled ∩ activated enablement (vendor env + site-admin config), validation kept for the Bar (Lavazh station retires with Car Wash), name stays parking-system, vision vehicle-category as an advisory anomaly flag.
- [[container-deployment]] — Docker images for the non-desktop apps: parking-server (Fastify API + bundled SPA via @fastify/static) + parking-vision (Python/uv ANPR); branch+SHA tags, per-env compose, Gitea registry, build-images.yml CI; pnpm deploy (not prune) for native better-sqlite3; migrate-at-boot. - [[container-deployment]] — Docker images for the non-desktop apps: parking-server (Fastify API + bundled SPA via @fastify/static) + parking-vision (Python/uv ANPR); branch+SHA tags, per-env compose, Gitea registry, build-images.yml CI; pnpm deploy (not prune) for native better-sqlite3; migrate-at-boot.
- [[fleet-deployment-komodo]] — fleet control plane: Komodo Periphery on each booth, driven by Komodo Core over a NetBird mesh, running the same compose files. Deploys manual + pinned to dev-<sha> (no webhook); secrets Komodo-managed per-booth+unique; booth.sh demoted to break-glass. Threat-model caveats: Periphery is a root agent (mesh-bound only), EVENT_SIGNING_KEY-in-Core is a fraud-root blast radius until ATECC608 signs. komodo/ is infra-as-code. - [[fleet-deployment-komodo]] — fleet control plane: Komodo Periphery on each booth, driven by Komodo Core over a NetBird mesh, running the same compose files. Deploys manual + pinned to dev-<sha> (no webhook); secrets Komodo-managed per-booth+unique; booth.sh demoted to break-glass. Threat-model caveats: Periphery is a root agent (mesh-bound only), EVENT_SIGNING_KEY-in-Core is a fraud-root blast radius until ATECC608 signs. komodo/ is infra-as-code.