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

9.1 KiB
Raw Blame History

type, tags, sources, updated, status
type tags sources updated status
decision
parking
decisions
open
finance
ledger
modules
subscriptions
carwash
2026-09-08 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