The case of the disappearing discount
17 May 2026 · 3 min read
The symptom
A customer messaged us after running through checkout with a LAUNCH15 coupon. The Configure page added their plan, applied the promo, and showed exactly what you'd want:
Plan $3.32
Promo (LAUNCH15) -$0.45
─────────────────
Total $2.82
They clicked through to Payment, scanned the USDT QR — and the wallet showed $3.32. The discount line was there, the math was right on screen, but the on-chain amount was the gross price.
That's the kind of bug that costs more than the discount it ate. If a customer pays $3.32 thinking they got a deal, the trust hit is bigger than the 45 cents we saved by refunding.
Tracing the money
The order pipeline writes through three records — Order → Invoice → Payment. Each step references the previous one, so to find where the discount disappeared I followed amount_cents from the top.
Configure-page submit hit OrderController::persistDraftAsCartOrder(). The controller computed subtotal_cents, discount_cents, and total_cents correctly in PHP — verified by dumping them mid-handler. But the row it created looked like this:
Order::create([
'amount_cents' => $subtotal_cents, // ← gross, before discount
'discount_cents' => $discount_cents,
// ...
]);
amount_cents was the gross. The discount was saved alongside but never subtracted into the column the rest of the system reads.
Two steps later, when the customer clicked Pay on Review, OrderController::store() summed amount_cents across each cart Order row to build the Invoice total. Invoice total then drove the Payment row, which drove the USDT QR amount, which drove what landed on chain.
The discount line in the Configure UI? That came from a separate computed property reading discount_cents directly. Two parallel paths, only one of which respected the promo.
The fix
Three places to update so net and gross stayed coherent:
OrderController::persistDraftAsCartOrder()— write net toamount_cents(subtotal minus discount). Thediscount_centscolumn stays for audit / receipt rendering.OrderController::store()review-step subtotal — subtract draft-orderdiscount_centswhen totalling. Without this, the controller would have re-derived a gross from the corrected rows during a re-render.Review.vuedraft section — mirror the livecartOrders[]rendering: show the promo line and net subtotal, not just the gross. Customers should see what the back end is going to charge, before they click Pay.
The test that locks it
Postmortems without a regression test are wishes. The end-to-end test now exercises:
Configure with a 15% coupon → Review → Store → Order persisted with net amount → Invoice picks up the net → Payment row carries the net → USDT QR encodes the net.
If any of those four hops drifts back to gross, the test fails before it ships. The discount is now a thing the system enforces, not just something the UI displays.
What we changed in our process
Two small things that would have caught this earlier:
- End-to-end tests across the order pipeline, not just per-controller. Unit tests on
persistDraftAsCartOrdercould pass while the column mapping was wrong, because the mapping was the bug. - Asserting on the on-chain amount, not the UI total. The test now reads
Payment::wallet_amountand compares to the expected net — the same number a customer will see in their wallet when they scan the QR.
What you should do
If you have an open invoice with a coupon applied and it was generated before this fix shipped (anything before v1.20), the QR may carry the gross amount. Email support and we'll regenerate it at the discounted rate — no questions asked, our bug, our fix.
For new orders, the Configure → Review → Payment pipeline is end-to-end coherent. You see what you pay.