• Mahmoud Aglan's avatar
    feat(financial): let the academy decide where every collected pound lands · e0b69a71
    Mahmoud Aglan authored
    Where a collection went in the ledger was a `match` statement: five itemable
    classes hardcoded to five revenue codes, identical for every client, and no way
    to express tax at all. An accountant who said "no, five pounds of that is a
    registration fee and the rest is training, and the kit carries 14% VAT" had no
    answer but a deploy.
    
    Revenue routing makes that answer data. Every kind of money the ERP can take is
    a source with one rule: a tax treatment, and destination lines that take a flat
    amount, a percentage, or the remainder. A rule can be narrowed to a single
    product, programme or event, which beats the academy default for that item
    alone. financial.revenue-routing edits them, with a preview that runs the
    unsaved rule through the real allocation engine rather than a second
    implementation that agrees with it until it doesn't.
    
    Three things it will not do:
    
    - Book tax as revenue. VAT collected is owed to the Authority, so it comes out
      first and credits a liability; the service refuses a tax account that is not
      one. Egypt's 14% is a per-source setting because some services are 10% and
      some are exempt.
    - Write an entry that does not balance. Integer piasters throughout, floors
      everywhere, one line closes the rounding, and assertBalanced() throws before a
      single row is written — a transaction is immutable, so a wrong one can only be
      reversed, never corrected.
    - Change anything on the day it ships. The migration seeds each academy the rule
      that reproduces its current behaviour exactly, so nothing moves until someone
      asks it to. The platform fee ships switched off for the same reason.
    
    Refunds now reverse the collection's own ledger rows in proportion rather than
    re-running today's rules — a rule edited last week would otherwise unwind money
    into accounts it never touched.
    
    LedgerAccountResolver::splitRevenue() and revenueCodeForItemable() are deleted
    rather than deprecated: a second implementation of "where does this money go" is
    one a caller would eventually reach for, and it would bypass every rule the
    academy wrote.
    
    Verified on a restored oc_sport tenant copy and on a database built from zero:
    521 tests, 0 failures on both the Postgres and SQLite suites. PaymentLedgerTest
    builds a schema without the routing tables, which pins the other half of the
    safety property — an installation that has not received the migration keeps
    taking money on the built-in behaviour instead of failing at the till.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    e0b69a71
Name
Last commit
Last update
..
components Loading commit data...
errors Loading commit data...
layouts Loading commit data...
livewire Loading commit data...
portal Loading commit data...
print Loading commit data...
public Loading commit data...
receipts Loading commit data...
reports Loading commit data...
website Loading commit data...
welcome.blade.php Loading commit data...