Skip to content
Skip to main content
Novel Systems home
Decision log
D-081August 5, 2026

The billing sandbox will not exhibit an invoice that does not add up

Affects: app/api/billing/checkout-session/route.ts.

What was wrong

D-080 fixed the adapter, and fixing the adapter did nothing for the page. The runner behind /integrations#billing-sandbox reuses the newest approved quote in the sandbox tenant — GET /api/v1/quotes?status=APPROVED&limit=1 — and creates a fresh one only when that returns nothing. The quote it kept finding was the first one ever approved, whose Stripe invoice is P49QOGVA-0003: finalised, marked paid, CA$0.00, no lines.

That quote can never be repaired. A finalised Stripe invoice cannot have items added to it, and both the approval fan-out and the manual repair path short-circuit on a sync row that already says SUCCEEDED — which this one does, because the empty invoice finalised without error. So the deployed fix was correct and invisible: the panel would have gone on rendering CA$0.00 forever, against a Checkout Session for $4,120.79 sitting two inches above it.

What was decided

Before reusing an approved quote, read its recorded invoice and check that it adds up. If it does not, do not reuse the quote — fall through to the existing create-and-approve path, which writes a new quote through the fixed adapter.

"Adds up" is self-consistency and nothing cleverer: at least one line, a total above zero, lines that sum to the total, and a subtotal plus tax that sum to the total. No comparison against the quote's own figures, because that would mean a second round trip to learn something the invoice already says about itself, and the failure this is guarding against — every field zero, no lines at all — is not subtle.

A null invoice is explicitly *not* unusable. Null means the invoice could not be read: a 403, a deployment holding no Stripe key, a timeout. Treating "I could not look" the same as "I looked and it was broken" would make a deployment that simply cannot see Stripe create a new quote every sixty seconds, forever.

Because the API lists approved quotes newest-first, the replacement becomes the quote the next request finds. So this creates one quote, not one per request.

The alternative, and why not

Delete or unapprove the poisoned quote in the sandbox tenant and leave the route alone. That is one SQL statement and it would have worked today. It was rejected for two reasons. It fixes an instance rather than a class — any future quote whose Stripe leg goes wrong in some new way would put the panel back where it was, and the next person would have to diagnose it from scratch. And it is worse product behaviour: a panel whose entire purpose is to demonstrate that billing works should decline to present a broken invoice as its evidence, no matter what wrote it. The check earns its place independently of the defect that prompted it.

What would have to change for this to be wrong

If a legitimate invoice could fail the arithmetic. Two cases are worth naming. Stripe discounts and credit balance would make lines sum to more than the total — neither is used here, and if either is adopted the sum must be taken against subtotal rather than total. And if Stripe Tax were enabled, tax would stop arriving as a line under our own label and taxCents would go null while the total still included it; the subtotal-plus-tax clause would then reject every invoice. Both are guarded by the same fact: taxCents null means *no line carried our label*, which is why the clause treats null as zero rather than as a reason to fail. Enabling Stripe Tax means revisiting this function first.