"Each Discount Works" Is Not "All Discounts Work"
Three price adjustments, each correct on its own, produced the wrong price in billing when they landed on one cart. Why a per-discount checklist cannot see that, and what to build instead.
Every discount on the cart was correct. The price was still wrong.
Not missing. Not zero. Wrong — by an amount that only existed when three things were true at the same time.
What the Ticket Actually Described
A step discount applied on top of a broadband bundle that already carried a service discount and a paid add-on. The discount was present everywhere you looked. It was on the cart, it made it into the payload, it appeared on the order.
The price that arrived in the billing system did not match.
Remove any one of the three adjustments and everything reconciles. Each of them, in isolation, produces exactly the number it should. Put all three back on one cart and the calculation breaks.
Highest priority, raised on the closing days of a sprint.
Two Failure Classes That Look Like One
The same feature area had already produced a cluster of defects a few weeks earlier, and every one of those was the same shape: a discount entered in the cart and then went missing somewhere downstream. Invoice, asset, document, change journey. One value, failing to survive the flow.
This was not that.
Here the value survives the flow perfectly. What breaks is the interaction between three price adjustments applied concurrently to the same order — an order-of-operations problem inside the pricing engine, which only has an effect when there is more than one operation to order.
Those two classes need different test designs, and the difference is worth stating plainly.
The first is a path. One value, followed until it stops moving. You can walk it.
The second is a space. Several values, present together, combined in a sequence somebody decided once and nobody wrote down. You cannot walk a space. You have to enumerate it.
Why Single-Variable Testing Cannot Reach It
Every axis had been verified on its own, and verified properly.
Step discount — checked. Service discount — checked. Add-on — checked. Product tier — checked. Four rows, four green results, four honest test cases with real assertions behind them.
The defect sits at an intersection that none of those rows visits.
That is what makes combinatorial defects so expensive to find late: the coverage report is accurate. It is complete, honest, and describes a set of single-variable claims. Nothing in a per-feature checklist has a column for at the same time as.
And the intersection is not exotic. A customer on a bundle, with a service discount, who buys an add-on, and then gets a step discount applied — that is a normal cart. Not an edge case anybody would have to construct. Just one nobody enumerated.
The Rule
A feature that applies more than one price adjustment to the same cart needs a stacking matrix, not a checklist.
"Each discount works" and "all discounts work together" are independent claims. Proving the first tells you nothing at all about the second. And the second one is the claim the business actually needs, because customers do not arrive one adjustment at a time.
The asymmetry is what hurts. The first claim is cheap to prove and gets proven early. The second is more expensive, gets deferred, and fails at Highest priority in somebody else's window.
How to Build the Matrix
The mechanics are ordinary. The discipline is in doing it before execution rather than after the first ticket.
- Name the dimensions before you name the cases. For that cart the dimensions were {step discount} × {service discount} × {add-on} × {product tier}. Write them as a header row, and the number of cells stops being a surprise — it becomes a number you can negotiate about.
- Make the assertion a reconciled figure, not a presence check. "The discount appears in the payload" passes on a cart that is priced wrong. Each cell needs an expected final price in the billing system, calculated independently, and compared.
- Reduce the matrix deliberately, and record what you dropped. A full cartesian product is often unaffordable, and pairwise coverage over the same dimensions is usually enough to surface an order-of-operations bug. What matters is that the cells you skipped were chosen, not forgotten.
- Ask the developer what order the adjustments are applied in. If the answer takes longer than a minute, or two people give different answers, you have found the risk before you have run anything.
The Cluster That Named the Missing Asset
There is a second version of this story, and it makes the point in numbers rather than in mechanism.
One week produced twenty-one new sprint bugs. Eight of them were discount and pricing defects: a £0 order total on a service change, a discount missing from the billing payload, wrong one-off costs when a custom discount applies, a wrong price on the invoice PDF.
Almost every one of those is a deterministic, data-driven check. A given discount on a given product tier produces a known order total and a known payload. That is functional, business-facing test work — the kind that belongs inside the sprint, at the cheapest layer that can hold it.
They surfaced as SIT regression findings instead.
Eight defects clustered in one feature area are not eight separate misses. They are one missing test asset — a discount × product-tier matrix that nobody had built. And the cluster landed in exactly the feature that an earlier defect analysis had already flagged as under-tested at the boundary, so the escape and the analysis were pointing at the same hole from two directions.
That is the part worth sitting with. The gap had already been named. What was missing was not the insight. It was the artefact.
Ask the Layer Question Before the Effort Question
When bugs cluster around one theme, stop triaging them one at a time. Two questions do more work than any amount of individual analysis.
What single test artefact would have caught the whole cluster? And why was it not run at the cheapest layer that could have held it?
Most pricing defects are matrix checks, not end-to-end journeys. They do not need a full order flow, an orchestration wait, or a downstream system to be up. They need a table, a set of expected numbers, and someone to compare them.
Running that table one sprint earlier is the difference between a two-minute in-sprint catch and a regression ticket at Highest priority on the day the sprint closes.
In the feature you are testing this week, how many adjustments can apply to the same record at once — and has anyone written down what happens when they all do?