The checkout bug that passed every happy-path test
Every checkout test was green. Then a customer combined two discounts and a gift card, and the order total went negative — and we paid them. Here's the case study.
This is a case study, details blurred, of a bug that taught my team why "all tests pass" and "it works" are not the same sentence. The checkout suite was thorough by any normal measure — and it sailed straight past a defect that, for a few hours, let customers get paid to shop.
Context
A standard e-commerce checkout. Users could apply a percentage coupon (SAVE20), a fixed-amount coupon (£10 OFF), and redeem store credit / a gift card. Each of these was a feature with its own tests. Each test passed. The suite covered the happy path for every discount type individually, plus the obvious validations (expired coupon, invalid code).
Symptoms
Finance flagged it, not QA: a handful of orders had a negative total. A few customers, intentionally or not, had ended up with the store owing them money — the order completed, the balance was negative, and store credit was issued for the difference. Small numbers, but exactly the kind of bug that makes the news if it spreads.
Investigation
Reproducing it took combining things no single test combined: apply the percentage coupon, then the fixed-amount coupon, on a low-value cart, then redeem a gift card. Each discount subtracted from the total independently, and nothing enforced a floor. On a £15 cart: 20% off → £12, then £10 off → £2, then a £5 gift card → −£3. The system treated −£3 as "we owe you £3" and issued credit.
Root cause
Two failures stacked. First, the discounts were composable without a floor — no rule said "total can't go below zero." Second, and the reason QA missed it: every discount was tested in isolation. There was no test for combinations, and specifically no test asserting the invariant that mattered — the payable total is always ≥ 0. The individual features were correct; the interaction between them wasn't, and nothing owned the interaction.
What the tests missed
The suite tested features, not invariants. "Coupon applies correctly" is a feature test. "No combination of discounts can produce a negative total" is an invariant — a property that must hold no matter what — and nobody had written it, because each feature team only saw their own piece. The happy path for each was green; the combinatorial space between them was untested and unbounded.
The reusable lesson
When multiple features touch the same number — a total, a balance, a count, an inventory level — test the invariants on that number, not just each feature. Ask "what must always be true here, regardless of the path?" and test that directly. And test feature combinations where they interact with shared state; the bugs live in the seams, not the features. This is the same "happy path proves nothing" trap as the 12 API bugs and a reason exploratory testing (trying weird combinations) earns its place.
Invariant & combination testing
- For any shared number (total, balance, count, stock), write the invariants — e.g. "total ≥ 0"
- Test discount/modifier COMBINATIONS, not just each in isolation
- Probe the boundaries: low-value cart, max discounts, stacked credits
- Assert what must NEVER happen (negative charge, double credit), not just what should
- Treat features touching the same value as one test surface — own the seam
- Add an exploratory pass that deliberately combines unrelated features
// RELATED QA.CODES RESOURCES
Template
Common Bug
// related
How a missing negative test caused a production incident
A negative quantity on a "remove stock" action inflated inventory and caused overselling. The feature worked; the absent negative test did not. Test the negative space at the endpoint.
The bug that only happened after daylight saving time changed
A case study: a scheduling bug that stayed invisible until the clocks changed — and the test scenarios that would have caught it.