Multi-currency donation, expense and accounting system
Delivered, in useDonations, expenses and accounts for an international aid charity
The hardest requirement on this system was not the accounting. It was that a long-serving volunteer treasurer, well into retirement, had to be able to read it, operate it and trust it — and that whoever the charity finds next has to be able to maintain it.
The client is anonymised at their request. Nothing here names the organisation, where it is registered, where it operates or anyone involved, and no financial figures, donor data or balances appear anywhere on this page.
The problem
Charity accounting is harder than it looks. Money given for a specific purpose cannot be spent on anything else, so restricted and unrestricted funds have to stay separated at every level. Transactions arrive in several currencies. In-kind donations need valuing before they can be recognised. Grants are tracked per programme. All of it feeds statutory reporting under a charity SORP regime, where the output is reviewed by an outsider.
The client is an all-volunteer charity running aid programmes in multiple conflict-affected regions, including physical shipment of in-kind goods at container scale. There is nobody in finance to absorb a design mistake, so a wrong assumption in the data model is not caught by a professional downstream. It surfaces at year end, in front of an independent examiner.
The system
A multi-currency ledger with restricted and unrestricted fund separation enforced in the schema rather than in reporting logic, so a fund cannot be misstated by a query written later.
Six bank statement parsers feed automated reconciliation. That is the difference between a treasurer spending an evening a week on the accounts and losing a weekend a month to them.
Per-programme grant tracking and in-kind valuation sit on the same ledger, so the statutory position and the programme position are one set of numbers rather than two spreadsheets that have to be argued into agreement.
Stack
- ~34k lines
Running
In production
The system carried a statutory year end. During that examination the independent examiner made fund reclassifications, which is the ordinary result of an outside review, and the schema absorbed them without a rebuild. Surviving that is the specific thing a fund model exists to do, and many do not.
The codebase runs to roughly 34,000 lines held at PHPStan level 9. That is the strictest setting the analyser has: at level 9 it refuses to let a value of unknown type be used at all, so every variable in the system has a type the tool can prove rather than one a developer assumed. Most PHP codebases sit around level 5. Unit tests run in Pest and the screens are driven end to end in Playwright.
It was produced by directing AI coding agents against a written specification, which is precisely why the verification bar sits where it does.
- The fund model survived outside review
- An independent examiner's fund reclassifications were absorbed during a statutory year end without a schema rebuild.
- AI-assisted delivery raises the verification bar
- Static analysis at level 9 of 9 across roughly 34,000 lines — no value of unknown type anywhere — plus unit tests in Pest and end-to-end runs in Playwright.
- The stack was chosen for the user
- PHP and MySQL, so a volunteer-run charity can maintain the system after the builder is gone.
Conviction
Nothing probabilistic sits in the path of the numbers
This system's output is statutory accounts that an independent examiner reviews. The ledger is deterministic and auditable, and it stays that way. AI reads, suggests and drafts. It does not post, calculate or file.
Knowing where not to put AI is a design decision rather than a limitation. Build time and run time are separate choices: this system was built by directing AI agents, and there is no model anywhere in the path of a posted transaction.
What you take
Domain modelling is the deliverable. The accounting rules are published and the implementation is commodity. What decides whether the system works is whether the fund structure was modelled correctly before anything was built.
The stack choice was the other half. PHP and MySQL were chosen deliberately over a fashionable stack, because this system has to outlive the person who built it and stay legible to whoever maintains it next. That is a judgement about the user, and it is not one a coding agent makes unprompted.
Ownership
Commissioned and paid for by the client. The repository, the documentation and the deployment pipeline transferred on final payment, with no ongoing dependency on me.
Roadmap
Design direction
Not built. These are on the roadmap, described here as intent rather than delivered work.
Plain-language queries over the ledger
The treasurer would ask a question in ordinary English and get an answer, rather than navigating reports built for accountants. It is the original constraint extended: the interface adapts to the user rather than the user adapting to the interface.
A first draft of the trustees' annual report narrative
Generated from ledger data as a drafting aid, then rewritten and signed by a person. It would never produce an output that goes near a filing unreviewed.
Start with a call
Twenty minutes to work out whether there is something here worth building. If there is not, I will tell you.