Skip to content

0016 — Settlements are summarised per obligation, and never per party

Status

Accepted.

Context

An Obligation records money owed. A SettlementReference records that it was settled somewhere else — on MTN, on Airtel, at a bank. The kernel holds no funds and is not the ledger of record for any of it.

Someone will ask what an obligation's settlement position is, and that question has an answer the kernel can give honestly. The adjacent question — what is a party owed in total — has an answer the kernel must refuse to give, because that number is a balance, and a system that shows balances is a system the National Payment Systems Act treats as holding funds.

The gap between those two questions is narrow enough that the shape of the code has to do the work, not a comment.

Decision

Summarise settlements for one obligation at a time. RecordView.settlement is attached to obligations only, computed on read, stored nowhere. There is no repository method, no service method and no route that takes a party and returns money.

Four things the summary is careful about:

Verification statuses are never added together. referenced_minor is a map keyed on verification_status, not a total. An asserted settlement is one side's claim that they paid; a provider_verified one is evidence from the rail itself. Collapsing them into a single figure launders the first into the second, and that distinction is the entire value of the repayment signal. disputed is surfaced separately again.

Currencies are never converted. A settlement denominated in something other than the obligation's currency is counted in currency_mismatch and excluded from the sums. Adding across currencies would require an exchange rate the kernel does not have and would have to invent.

Arithmetic is integer minor units end to end. Money.amount_minor is an integer for the reason the schema gives, and this is exactly the aggregation where binary floating point error accumulates silently. The SQL sum is bigint, carried as text and parsed once.

The remainder is called unreferenced_minor. Not outstanding, not balance, not owed. The kernel does not know whether money moved; it knows whether a record exists claiming it did. The name should say only that. It is not clamped at zero, so over-referencing shows as a negative rather than disappearing.

Consequences

Superseded and retracted settlements are excluded, as everywhere else. A corrected settlement references once, at the corrected amount.

Two tests hold the line rather than a convention: one asserts no method on RecordRepository is named anything matching balance|wallet|float|owed|payable|receivable, and one asserts the summary carries no party-shaped field. They fail the moment someone adds partyBalance(), which is the only form this mistake takes.

The lender-facing view in the seed work builds on this per obligation, listing them, and does not total them. A list of obligations with their settlement positions is a credit file. One number at the bottom is a balance sheet.