Skip to content
← All systems

Multi-currency donation, expense and accounting system

Delivered, in use

Donations, 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.