Why staged payments break accounting systems
Most systems store one paid date per bill. A payment in stages never writes it, so every screen asking "is this paid?" goes blind on exactly the biggest orders.
Almost every purchase system in existence stores, somewhere, a field meaning this has been paid. Usually a date. Sometimes a flag.
It works perfectly for a bill that is paid in one movement, which is most bills. It fails completely for a bill paid in stages, which is most of the money. Staged payment is not an edge case in construction, because money arrives on one ladder and leaves on another — two payment ladders that never line up.
The structural problem#
When a payment has a schedule, the stages are the payment record.
An advance in August, a balance in September. Two entries on a schedule, each with an amount, a date and a reference. Between them they describe the whole payment. There is nothing missing and no information has been lost.
What has not happened is the writing of a single top-level paid date, because there is no single date to write. The payment did not happen on a day. It happened across two.
So a system carrying both mechanisms — a paid date for simple payments and a schedule for staged ones — has two representations of the same fact. And every piece of code in it must know which representation to ask.
They do not. Not because anybody is careless, but because the paid date is right there, obvious, well named, and correct for most rows, and the schedule requires you to know it exists.
What it looks like when it goes wrong#
This happened to us, and the shape of it is worth setting out because it is almost certainly happening somewhere in your own tools.
An order was paid in full — an advance, then the balance some weeks later. Every rupee had gone. The people who paid it knew it was paid.
A queue inside the software kept asking for it to be paid again.
The cause was exactly the structure above. The instalment function never wrote the top-level paid date, so any query asking is the paid date empty? was blind to every rupee that had ever moved in instalments. One screen asking the wrong question is an ordinary defect. What we found was the same question asked all over the application, place after place, each in its own words, each written by somebody who reasonably assumed the field meant what it looked like it meant.
Four of them were queues and counters: the list cleared to pay, the list waiting for a signature, the number in the sidebar, and the sweep that puts a decision back in front of somebody who has already made it. Five more were found afterwards.
The full account, including the automated test that was holding the bug in place, is in a fact that had no owner.
The three downstream failures#
The queue was the visible symptom. The expensive parts were downstream.
A chaser that had lost its credibility. A material line on an order paid in stages sat at "waiting on payment" for ever, and that stage has a reminder attached which rings the finance team every few days. So Finance was being reminded, repeatedly, about money that had already left the account.
Nobody reported it. That is the part worth sitting with. A recurring reminder which is occasionally wrong teaches you to skim the whole class of reminder, and once you skim it, the one that matters costs the same as the ones that do not.
A lorry that arrived unannounced. The same stale question was being asked at the gate. The guard's list of expected loads refuses any line whose money has not settled, which is sensible. A load paid to the last rupee in stages carried no paid date, never reached the list, and a fully-paid lorry turned up at the barrier with nobody expecting it. That is the exact event the screen exists to prevent.
A matcher that never let go. Our bank-line matcher kept staged-payment tasks in its candidate pool for ever, at their full amount, because the stages never set the paid date. So a long-settled order stayed permanently available to be matched against some future incoming transaction. The fix was to treat a fully answered schedule as a settlement timed at its last stage.
Three completely different subsystems. One missing fact.
The fix is not one fix per screen#
Writing the correct condition into each of those places would produce a copy of a rule per screen, which is the same problem again with a longer fuse.
There is now one function that answers has the money for this actually settled?, living in the file that owns payment schedules, with a mirror of it in the database query language beside it so the two cannot drift apart. Every surface asks it. None of them holds a second opinion.
That is the general remedy for this class, and it applies far beyond payments: any fact your work depends on that is computed in more than one place will eventually disagree with itself, and it will not disagree loudly. The same argument, applied to balances rather than to flags, is in retention money is a balance, not a memory.
What to check in your own system#
Take one order that was paid in two parts, and ask each of these questions of your software:
- Does it appear as outstanding anywhere?
- Does any reminder still fire for it?
- If you export a list of unpaid bills, is it on it?
- Does the ageing report include it?
- If somebody searched for it today, would the answer be the same on every screen it appears on?
If the answers differ, you have the same structure, and the number of places it has leaked into will be larger than you expect. Ours was nine.
The design lesson#
Do not represent one fact two ways.
If staged payment is possible at all, then the schedule is the payment record and the top-level date is a derived convenience at best — and a derived convenience should be computed, not stored, because a stored one goes stale in silence.
And if you must have both, then no code anywhere should read the raw field. It should ask a single function, and that function should be the only thing in the system that knows both representations exist. Everything else asks it.
A fact worth trusting has one owner.