A discount on one line stranded money on the others
Refund a discounted dish and the till then refused to return the rest of the bill, short by exactly the discount. The sum had three homes. Now it has one.
Here is a bill. One dish at £10 with £2 off, so £8. Another dish at £5. Total £13. The customer wants the discounted dish refunded, and the till hands back £8, which is right. Then they want the £5 dish refunded too, and the till says no: that is more than is left on this receipt, £3 can still go back.
The customer is short by exactly the discount.
Two sums that should have been one
When the till refunds a line it pays back what the customer actually paid for it, net of the deal. When it works out how much of a receipt is still refundable, it was adding up the lines at their gross price. So after the £8 went back, the receipt looked £10 refunded, and only £3 of the £13 appeared to remain. The more discounts on a bill, the more money the till would refuse to return.
No live venue had refunded a discounted line yet. This was a bug waiting for the first one.
Why the tests did not see it
The function that works out what is left on a receipt was well tested, and every one of those tests passed, and they were right to. Given consistent figures it gives the right answer. The mistake was in how the route filled those figures in, and a test of a pure function cannot see its caller. A thoroughly tested calculation proves nothing about the arguments it is actually handed. That is a lesson worth more than the fix.
One home for the sum
The net value of a line was being written out by hand in three places: once in the refund, correctly; once in the what-is-left feed, wrongly; and a third copy in the tax calculation. It now lives in one place, next to the two functions that already owned its halves, and all three callers use it. There is no longer a way for one copy to drift from the others.
What we pinned
The property that matters is simple: after both refunds, the refunds on that receipt add up to £13, the bill exactly. Not a penny stranded, not a penny extra. We also pinned that the cap still binds afterwards, because the £5 going back is also true of a till that has stopped capping refunds at all, which would be a worse bug than the one we fixed. And we pinned that a line refund hands back the net, which was always right, so a later tidy-up cannot bring the correct half into line with the wrong one. Reverting the fix fails the two tests that name the stranded money while an ordinary undiscounted bill behaves exactly as it always did.