Files
parking_solution/wiki/decisions/party-ledger.md
T
julian 4fd175e0e4 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
2026-09-09 10:19:09 +02:00

150 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]]