Billing & Payments
Billing describes how Olly invoices accounts and tracks money. Charges are what is owed, invoices group charges for payment, and a ledger records settlement. The ledger is designed as a double-entry record, but charge debits are not yet posted, so today it captures payments and adjustments. Owned by the billing service. All amounts are NUMERIC(14,4) and currency is CHAR(3) DEFAULT 'GBP'; this is a single-currency (GBP) domain today.
Field reference: full columns, types and nullability live in the catalog: glossary terms
Invoice,InvoiceLineItem,Charge,Payment,Adjustment,LedgerEntry,InstallmentSchedule,AutopayPreference,EOBReference. This page is the narrative.
The double-entry ledger
In the design every financial movement posts a LedgerEntry; today payments and adjustments post entries, while charge debits (the CHARGE entry type) are defined but not yet written. An entry has an entry_type, a direction (DEBIT = owed to Olly, CREDIT = owed by Olly), an amount, and a polymorphic reference: reference_id (UUID, NOT NULL) + reference_type (TEXT, NOT NULL); there is no database FK, the pair identifies the originating charge/payment/adjustment by convention.
The intended account balance is the net signed sum of entries (SUM(DEBIT) − SUM(CREDIT)); because charge debits are not yet posted, the ledger currently reflects settlement (payments and adjustments) only. Reversals are new entries with the opposite direction referencing the original; the original is never mutated.
The money flow
Chargeis the atomic billable event (a premium installment, a fee), carryingpolicy_id/term_idfor policy linkage. (TheCHARGEledger-entry type is defined but charge debits are not yet posted to the ledger.)InvoiceLineItemreferences exactly oneCharge; its amount is copied from the charge as a denormalisation for invoice rendering.Invoicegroups line items for an account and a period. Itstotal_amountshould equal the sum of its line items, an application-layer invariant, not a database trigger (there are no triggers in the billing schema).Paymentsettles exactly one invoice (invoice_idis a NOT NULL FK; there is no multi-invoice allocation).statusdefaults toSETTLEDandsettled_atis NOT NULL;methodis free text (conventionallyDIRECT_DEBIT/CARD/BANK_TRANSFER), andreferenceholds the processor reference. Each payment posts aCREDITLedgerEntry.Adjustmentis the mechanism for off-cycle money: a manualDEBIT/CREDIToutside the invoice cycle (a pro-rata refund on mid-term cancellation, a goodwill credit), each posting a balancingLedgerEntry. Refunds are Adjustments / reversing ledger entries, not negative payments.
Invoice lifecycle
status is free TEXT (no DB CHECK); the convention is DRAFT → FINALISED → PAID (or VOID), with finalised_at/paid_at recording the transitions.
- An invoice transitions to
PAIDwhen the sum of itsSETTLEDpayments is>= total_amount; an overpayment also marks it PAID; exact equality is not required. - Delinquency → lapse:
due_dateplusgrace_period_days(DEFAULT 30) drives delinquency. Past the grace window an unpaid invoice is markeddelinquent_at, and ultimately the policy lapses (lapsed_at), the business purpose of the grace period.
Installment schedules
InstallmentSchedule splits a premium across installments invoices at a frequency of MONTHLY | QUARTERLY | ANNUAL (free text), scoped to a policy_id. The billing scheduler generates an invoice per due date; by convention the final invoice absorbs any rounding remainder so the installments sum exactly to the total.
Autopay
AutopayPreference is per policy (policy_id is UNIQUE), with enabled (BOOLEAN, DEFAULT FALSE) and a nullable free-text method. When enabled, billing initiates collection on the invoice due date. (There is no retry-strategy column or stored-payment-method FK; the earlier exponential-retry machinery was not real.)
Invariants
- The ledger records payments and adjustments (charge debits are not yet posted); invoices are built from charges and their line items.
Invoice.total_amount= Σ line items, and an invoice isPAIDonce Σ SETTLED payments ≥total_amount, both app-enforced, not DB-enforced.- Each
InvoiceLineItemreferences exactly oneChargeand copies its amount; eachPaymentsettles exactly oneInvoice. - Every payment and adjustment posts a balancing
LedgerEntry; reversals add opposing entries rather than mutating. Charge debits are not yet posted.
Caveats
- Statuses are conventions, not enums.
status(andentry_type/direction/method/frequency) are freeTEXTwith no DB CHECK on every billing table; validation is the app's job; the database accepts any string. - No triggers. The sum=total and PAID rules are enforced in application code, not by Postgres triggers.
- Socotra alignment is entity-shape only. Socotra models invoice lifecycle as
invoiceState(open | settled), not Olly'sDRAFT/FINALISED/PAID/VOID(the billing domain also usesDELINQUENT/LAPSED).
