A new switch over old data is not a feature: it is a migration
The settlement module was built, reviewed, deployed and tested. One thing was missing: flipping the flag in production.
It is a one-character change. false to true.
Before touching it I asked the question that was not in the ticket: what moves on the first run?
Nobody had asked. And the answer was one database dump away.
What was waiting on the other side of the switch
Twenty-five credited, unsettled orders. Months of payments coming in with nothing going out, because the process meant to move them did not exist yet.
The number was not the important part. The important part was that those twenty-five were not one group. They were two opposite problems living in the same table.
Some had a release date already in the past. Flip the flag and the process would take them all at once, on the very first run, in a single batch. Months of money moving in one go on an ordinary Tuesday, because the clock said so — without any person having decided that today was the day.
The others had no date at all. And those the process would never pick up. The date is stamped at crediting time; turning the automation on does not backfill. They would stay held forever, silently, without an error in any log, without surfacing in any alert. Merchant money frozen by omission.
One switch. One group that fires without permission and another that never fires at all. Both behaviours are the same bug: nobody looked at the prior state.
A third one turned up along the way: the held balance exceeded what the orders claimed by the exact amount of one of them. A duplicated credit. Not a rounding gap, not a fuzzy mismatch — one order counted twice.
Why new software cannot see this
When you build a feature your head is in the flow: a payment arrives, it is credited, the date is stamped, the term matures, it is released. You write tests for that flow. They all pass. The code is correct.
And it is. For the data that will be born after it.
The problem is that an automated process does not debut against an empty system. It debuts against everything that happened while it did not exist. That history never honoured your invariants, because your invariants did not exist when it was written.
A new switch over old data is not a new feature. It is a migration. And a migration nobody called a migration is a migration nobody reviewed.
What we did instead of flipping it
Separate before moving. What moves by itself down one path, what never moves down another:
- The overdue ones → to the automation, but deliberately, knowing how much and when.
- The ones the process would never take → to a manual desk in the back-office, where a person sees them and decides. There is no way to automate something that is missing the very field the automation needs.
Two more fixes came out of looking closely:
The settled amount is the net that actually came in, not the gross the payer submitted. Moving the gross would have released fees that do not belong to the merchant. They are two similar columns in the same row, and picking the wrong one gives money away systematically and silently.
The automation flags moved out of the back-office tree. They were hanging off the same node as the menu. Switching off a screen must never switch off the engine that moves money: those are two different decisions and they deserve two different switches.
The question I ask now
Before turning on any automated process over money:
How many rows will it take on its first run, and how many will it never take at all?
If you cannot answer that with a query, you are not ready to turn it on. Not because the code is wrong — it probably is not. Because you do not know what you are releasing it against.
The short version, for the wall: turning it on is instant, undoing it does not exist.
(In LedgerCore every movement is an entry with its date and its reference, so "what will this process move" is a query rather than archaeology. That is the whole difference between measuring and guessing.)
Comments
No comments yet. Be the first.