← Blog
COMPLIANCE4 min read

We audited our own invoice tests. A green suite was hiding a guard that read the wrong number.

Aug 21, 2026DekimuAI-generated

We spent four days auditing the test coverage behind id.dekimu.com's own invoicing engine — the system that issues Dekimu Labs' own invoices under Spain's e-invoicing rules — and found real bugs hiding behind passing tests. The sharpest one: a guard meant to skip invoicing when there's nothing to collect was reading the wrong number, and could silently skip a real, billable supply.

A guard reading the wrong field

The rule is simple: no supply, no invoice. When a subscription starts on a free trial, nothing has been supplied yet, so nothing should be numbered. We built a guard for exactly that case, and it worked — for the case we tested. A trial start looks like this in Stripe's own terms: total is zero, and amount_due is zero too. The guard checked amount_due.

Those two figures only look interchangeable because the case we had in mind made them agree. amount_due is what Stripe still expects to collect after credits are applied. total is the value of what was actually supplied. They diverge the moment a customer has a credit balance — a self-serve plan change through Stripe's billing portal can leave proration credit sitting on an account, and the next invoice finalizes at total: 119,79€, amount_due: 0€. A real supply happened. Nothing was left to collect for it. The old guard read that as "no supply" and skipped issuing anything: no number reserved, no PDF, no register row, nothing to find it by later.

Why a skipped invoice is worse than a burned number

Under Spanish invoicing regulation (RD 1619/2012, art. 2), a supply requires an invoice. A burned correlative — a number reserved and then voided — at least leaves a trace that something happened and was handled. An invoice that was never issued for a real supply leaves nothing: no entry in the sequence, no record to reconcile against later. We changed the guard to read total instead of amount_due. That's a one-line fix. Trusting the fix took longer.

The tests that passed for the wrong reason

The first version of the counter assertion — the check meant to prove an invoice number actually gets reserved — passed with the buggy guard removed entirely. That looked like confirmation. It wasn't: the entitlement test harness never stubbed the Stripe calls the code needed, so nothing reached the number-reservation step whether the guard existed or not. The test was catching a crash, not a burned number. We moved the assertion into the composition suite, the one place a finalize actually runs through to issuance, and mutation-tested both directions: delete the guard and the counter climbs from 6 to 7 (an invoice that shouldn't exist gets numbered); swap total back to amount_due and it stays at 6 (a real supply goes unnumbered, reproducing the original bug). Only then did the test earn the claim.

The failure mode is never "the assertion is wrong." It is "the assertion is fine, and it is pointed somewhere else."

The same shape turned up twice more in the same sweep. A credit-note render-failure handler set one status field but not the other it was supposed to travel with, so a document whose PDF failed to render could get stranded — a burned correlative pointing at a placeholder URL, unreachable by either re-render path, while its own code comment promised a fix that never ran. And a test written to confirm the repair used a lookup that silently returned an earlier test's row instead of the one under test, reporting success on data it never checked. Neither suite was lying. Both were reading past the thing that mattered.

What's still open

The guard fix and the corrected tests are merged. What's deliberately not done yet: a one-shot backfill for credit rows written before the fix existed — every one of them is a Stripe refund, so the correct value is uniform and known, and inventing a default inside the running code would just reintroduce the guess this fix removed. It's filed as a named prerequisite before the next stage of this system unparks, not folded silently into the merge that fixed the live path.

COMPLIANCE

This post was drafted by an AI system from Dekimu's public engineering record and published with automated checks, without per-post human editing.

← Back to blog