Let's talk
engineering

A zero tax rate fell through to a hardcoded default, because zero is falsy

The tax rate used on a sale was resolved like this: take the rate sent with the request, otherwise the organisation’s default rate, otherwise a number typed into the handler.

const effectiveRate = requestRate || organisation.defaultRate || 18.00;

A zero-rated item is a real thing. Some goods carry no tax, some customers are exempt, and some organisations are below the registration threshold entirely. Every one of those cases wants a rate of zero, and every one of them sends a zero, and zero is falsy.

So a sale that should carry no tax skips the first fallback, arrives at the organisation’s default — which is also not zero, because it was set when the account was created — and, if that were somehow absent, lands on a rate that a developer typed into a route file.

The three separate mistakes in one line

Zero is a value. || selects the first operand that is truthy, and for numbers that excludes zero. For any quantity where zero is meaningful — a rate, a discount, a quantity, a threshold, a count — || is the wrong operator and ?? is the right one. This is a well-known trap and it keeps happening because the two read identically in a code review.

The deeper version of the mistake is that “absent” and “zero” were never distinguished in the input. If the field is optional, the handler needs to know whether the caller declined to specify a rate or specified a rate of nothing. Those are different requests. Once the field arrives as a plain number with a fallback chain behind it, that distinction is gone before any logic runs.

A statutory number does not belong in a route handler. The final fallback is a rate that was current, in one country, on the day someone wrote that line. It has no comment, no date and no source. The same number appears as a column default on the organisation table, and again — under a different column name — as a default on the purchase table. Three copies of one jurisdiction’s rate, in three places, none of which knows about the others.

That is a fossil, and it is the single most reliable way to tell what country a product shipped in first. It is harmless while there is one country and it is a migration the day there are two — which came up on this system, because the product was being prepared for a market with a different rate, a different currency and a different set of identifiers on the invoice.

A fallback chain hides missing configuration. The whole point of the third fallback is that the code never fails. An organisation with no rate configured produces invoices anyway, at a rate nobody chose, and nothing anywhere reports that the setting is missing. The failure is silent by design.

If a tenant has not configured a rate, the correct behaviour is to refuse to issue the document and say which setting is missing. That is an annoying error on the day a tenant is onboarded, and it is much less annoying than discovering a month of invoices at the wrong rate.

What the schema got right

One thing in this design is correct and worth stating on its own, because it is the part people leave out.

The rate is stored on the sale record. Not looked up from the organisation when the invoice is printed — copied onto the document at the moment it is created, alongside the computed tax amount.

That matters because rates change. When a rate changes, every document issued before the change must continue to show the old rate, and every report over a period spanning the change must use each document’s own rate. A system that stores only the current rate on the settings record, and computes tax at display time, silently rewrites its own history the day the rate moves.

Anything that goes onto a financial document is a snapshot, not a lookup. The rate, the customer’s address, the item description, the unit price, the seller’s registered identifiers. All of them belong on the document, copied at issue, because all of them can change in the master record afterwards and none of the changes may reach back into a document that has already been issued.

The cost is duplication, and people resist it for that reason. The duplication is the feature. A foreign key to a mutable row is a promise that the row will not change, and nobody can keep that promise.

The rate that was stored and the rate that was applied

There is one more wrinkle in this system, and it is the kind that produces an unanswerable support question.

Whether tax is computed at all is decided by a boolean on the organisation — a flag saying the business is registered. When it is false, the tax amount is zero. But the rate column on the sale is still populated, with the default, because the two lines are independent.

So there are documents in that table carrying a non-zero rate and a zero tax amount. Anyone reconciling the table by multiplying the taxable amount by the stored rate will get a figure that does not match the stored tax amount, on every row for every unregistered business.

If two columns are supposed to be consistent, either derive one from the other or constrain them. Storing a rate that was not applied is storing a number that is not true of the document. The honest version is to write a zero rate when no tax was charged, so that the arithmetic on the row holds without needing to know about a flag on another table.

The rules

Use ?? for anything where zero, empty string or false is a legitimate value, and use || only where you genuinely mean “any falsy value should be replaced”. In practice that second case is rare enough that a lint rule banning || on numeric defaults is worth the noise.

A configuration fallback chain must end in a failure, not a value. Two levels of fallback are a design. Three levels ending in a literal is a way of never finding out that the configuration is missing.

Every number that carries a jurisdiction inside it — a rate, a threshold, a rounding rule, the start month of a financial year — is tenant configuration on day one, even when there is only one tenant and one jurisdiction. Making it configurable later means finding every copy, and the copies will be in a column default, a model default, a route handler and a report, under four different names.

The limit worth admitting: the existing rows were left as they were. Backfilling a rate onto historical documents means deciding what the rate was, and the only honest source for that is the document itself. Where the stored rate and the stored amount disagree, the amount is what the customer paid, and that is the number that stays.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch