Engineering · The back office · Money

Two taps, two codes, both spendable

A double press on Issue voucher made two vouchers with two codes, each worth money. The fix went into the one place all fifty-eight dialogs pass through.


We have been walking the back office four screens at a time, pressing each dialog's main button twice in one gesture and counting what reaches the server. Most of the time the answer is one write, because the handler disables its button before the first request leaves. On the vouchers screen the answer was two. Two requests, two vouchers, two codes, both spendable at the till.

Why this one was the worst of the walk

A duplicate supplier is a nuisance. A duplicate category is refused by the database, because the name is unique, so the second press shows an error on a save that worked. A voucher's code is invented on the server, so no uniqueness rule could ever have caught it, and a voucher is money. Somebody meaning to issue ten pounds issues twenty, and nothing on the screen says so.

Two screens on the same walk came back as not measured rather than as passed. Their first button adds a row to a list held in the page and sends nothing. Zero writes on the wire is not a passing guard. It is a probe pointed at the wrong control, and writing it up as a tick is how a real bug gets a green mark.

Fix the fifty-ninth caller before it exists

A search found fifty more dialogs built the same way. We did not patch fifty handlers. Every dialog in the back office and the till is built by one function, and a comment above it already made the argument: a rule written into one place cannot be forgotten by the fifty-ninth caller. So the guard now sits on the dialog's footer, ahead of the button's own code, and drops a repeat press while a request is in flight.

The failure mode that would be worse than the bug is a button that never comes back. A refusal frees it the same instant a success does, because the counter is released in the same block either way. If nothing was ever sent, it frees after four hundred milliseconds, since the bug is one gesture tens of milliseconds wide, not a considered second attempt. And there is an eight second ceiling regardless, because a request that never answers is not the same as one that failed, and a button should not wait for ever on a reply that is not coming.

Driven with four controls: one press sends one, two presses send one, a later press still sends one, and Cancel is never swallowed. With the guard taken out, the second case sends two again. The other fifty dialogs are covered by the mechanism and have not been driven, and the walk file says exactly which three were.


Try it on tonight’s service.

Nothing to install, no card. Not better by the weekend? Close the tab.