4fd175e0e4
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
150 lines
9.1 KiB
Markdown
150 lines
9.1 KiB
Markdown
---
|
||
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]]
|