BlogLessons

The test mode didn't test

We sent test mode on every order, but the invoicing API never checked: the account decides that

Nicolás Biondi

A lone brass bell on a dark desk, crossed by a cyan beam and a violet glow.

In early August I put together a test order with made-up data, left test mode on, and fired the first rehearsal of our integration with the e-invoicing provider, the API that issues receipts with legal weight and no undo button. The API processed the order like any other: same response, no test marking anywhere. Whether it was a rehearsal or not depended on something I hadn't looked at. My test flag rode along in the request fully convinced of its own authority.

We trusted a flag

The issue button kept test mode on by default, and I assumed that this per-request flag decided whether the document was real. Nobody went to check. A flag called test projects a confidence it has done nothing to earn.

The API ignored the flag and said nothing

The first thing I checked was the field name, convinced we had spelled it wrong. It was spelled fine. The API returned no error and no warning: it accepted the flag and ignored it completely. The documentation said so in plain words; I just hadn't read it. The environment comes from the settings of the account you hold with the provider, and it doesn't travel in any request.

It's like writing "VOID, PRACTICE ONLY" in the memo line of a check and trusting that. The bank reads the account and the amount, and pays. If you want to know whether the check is real, you call the bank and ask which account it came from.

Meme Hide the Pain Harold: I sent the test mode flag / The API read it and ignored it I sent the test mode flag The API read it and ignored it

Request with test flag

Invoicing API

Flag ignored

Environment: the account's

Effects per the account

Test mode rode in the request; the environment lived in the provider's account

A crude lock

The mitigation was ugly on purpose: while the local configuration says "test", the code blocks issuing completely. I preferred a blocked issue button over a rehearsal that might not be one.

Dry run, verified environment and idempotency

When the provider published a new version of their API, I swapped the lock for something with judgment, because it brought the three missing pieces. A dry-run endpoint, the real rehearsal: it runs every validator and issues nothing. A call that validates the access token and answers which environment the account is in, the source of truth instead of my assumption. And mandatory idempotency keys: a unique identifier per order, so a retry after a timeout returns the original result instead of issuing twice.

The key derives from the order and always comes out the same, so an ambiguous timeout stopped being a roulette wheel. You retry with the same key and the provider answers with what it already did, without doing it twice.

79 of 79 and zero documents issued

I verified the full migration against the real API without issuing a single document: the whole contract was tested through the dry run and the environment check. The integration's test suite closed at 79 of 79 green.

The gate ended up like this: before every issuance, the system asks the provider which environment the account is in and compares it with its own configuration. If the two answers don't match, or if there's no answer, it issues nothing. It fails closed on purpose.

The test mode flag still rides in every request, out of habit. We no longer believe a word it says: the environment is answered by the only one who truly knows it, and it gets asked every time.

invoicing postmortem idempotency erp-integration