feat(settlements): settle a member's account instead of editing around it
A club that ran on paper for years does not arrive in the system as a
clean ledger. On the first academy live, 33 players were registered with
an invoice raised and the "pay now" toggle left off — 25 of them in two
data-entry evenings — and 27 of those are now carrying an unpaid
registration month plus an unpaid September renewal. Nine paid for the
federation card in instalments typed into free-text lines. Eight invoices
were issued at zero because no price existed yet. Three people exist
twice. None of that is a bug in one screen; it is a whole class of file
that reality got ahead of.
The desk had four tools that each did a slice: collect a payment, correct
one invoice's amount, back-fill missing invoices, register someone who
started months ago. None of them answers the question an operator has in
front of a parent — this file is wrong in several ways at once, what do
we do about all of it — so corrections were made wherever a screen
allowed them and the ledger drifted further.
SettlementService applies a reviewed set of corrections as one
transaction and one record: money taken and never entered (on the day it
was actually taken), a month closed for less than it was billed because
the player joined halfway through, a month dropped entirely, a month
nobody billed, a card or kit sold outside the system, a free-text line
linked to the product it was really paying for, an agreed instalment
plan, a payment sitting on the wrong month, and an overpayment held as
wallet credit. Money moves through PaymentService so the ledger, the
balance and the receipt all happen; stock through InventoryService; a
waiver is written as the admin_override the roster already knows how to
explain, leaving subtotal_amount alone so "650 of 900, discounted" still
reads. Nothing calls auth() or session(): actor, branch and amounts are
parameters.
AccountAnomalyScanner finds the files rather than waiting for an argument
at the desk — seven cases, worst first, each with the sentence that says
what to check. SettlementWorklist lists them with a CSV export;
AccountSettlementWizard puts one account on a page, proposes the
corrections that fit what it found, shows exactly what will be collected,
waived and billed, and demands a written reason before it writes
anything.
Both screens are gated on a new settlements.manage permission — waiving a
month is the academy's call, and an owner should not need a platform
administrator to make it — delivered by migration as well as seeder,
since db:seed only runs on a first deploy.
Two things the tests caught rather than production: Postgres refuses FOR
UPDATE on an aggregate, so numbering settlements from max(id) would have
rolled back a whole settlement the operator had already confirmed; and
payment_plans_status_check has no 'partial', so a part-paid plan is
active with the count saying how far along it is.
Verified against a restored oc-sport tenant: 20 settlement cases and 7
render/permission cases pass, including cross-participant access, a
future date, an oversized payment, and a failing second action rolling
the first one back. Full suite 298 tests, no failures.
Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Please register or sign in to comment