Skip to content
Updated Jun 9, 2026

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

  1. Charge is the atomic billable event (a premium installment, a fee), carrying policy_id/term_id for policy linkage. (The CHARGE ledger-entry type is defined but charge debits are not yet posted to the ledger.)
  2. InvoiceLineItem references exactly one Charge; its amount is copied from the charge as a denormalisation for invoice rendering.
  3. Invoice groups line items for an account and a period. Its total_amount should equal the sum of its line items, an application-layer invariant, not a database trigger (there are no triggers in the billing schema).
  4. Payment settles exactly one invoice (invoice_id is a NOT NULL FK; there is no multi-invoice allocation). status defaults to SETTLED and settled_at is NOT NULL; method is free text (conventionally DIRECT_DEBIT/CARD/BANK_TRANSFER), and reference holds the processor reference. Each payment posts a CREDIT LedgerEntry.
  5. Adjustment is the mechanism for off-cycle money: a manual DEBIT/CREDIT outside the invoice cycle (a pro-rata refund on mid-term cancellation, a goodwill credit), each posting a balancing LedgerEntry. 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 PAID when the sum of its SETTLED payments is >= total_amount; an overpayment also marks it PAID; exact equality is not required.
  • Delinquency → lapse: due_date plus grace_period_days (DEFAULT 30) drives delinquency. Past the grace window an unpaid invoice is marked delinquent_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 is PAID once Σ SETTLED payments ≥ total_amount, both app-enforced, not DB-enforced.
  • Each InvoiceLineItem references exactly one Charge and copies its amount; each Payment settles exactly one Invoice.
  • 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 (and entry_type/direction/method/frequency) are free TEXT with 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's DRAFT/FINALISED/PAID/VOID (the billing domain also uses DELINQUENT/LAPSED).

Olly Health Insurance Platform