• Mahmoud Aglan's avatar
    feat(settlements): settle a member's account instead of editing around it · e45bd6d7
    Mahmoud Aglan authored
    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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    e45bd6d7