Tax And VAT
Published
Tax is the part of a checkout where being quietly wrong is worse than failing. A refused payment gets fixed the same day; a rate applied to the wrong orders accrues for a quarter and arrives as an assessment. The rules deciding it are property names on an options object and flags on jurisdiction rows. We protected a file that configures a tax engine, renamed the names a group at a time, and read the money.
The configuration, and where the money is
The storefront quotes VAT-inclusive prices, because European consumer law requires the displayed price to be the price paid. That single fact makes the engine's job subtraction rather than addition: back the tax out of the quoted figure instead of adding it on top. Four jurisdictions are configured with their own rates. One of them, an Irish business-to-business rule, carries a reverseCharge flag, because a VAT-registered buyer in another member state accounts for the tax itself and the seller must charge nothing. A school is on an exemption list, and the application supplies its own rounding function so the engine agrees with the invoicing system.
Unprotected, a 120 pound UK order is charged 120, of which 20 is VAT. A 1000 euro Irish B2B order is charged 1000 with zero tax and the reason reverse-charge(ie-b2b). The exempt school pays 500 with no tax. Every figure is printed: what was charged, what is tax, and what the seller keeps.
The sharpest result is the one nobody can see
Renaming reverseCharge -- one boolean on one jurisdiction row -- put 186.99 euro of VAT on the Irish order. That is tax the seller has no right to collect and cannot remit, taken from a business customer who will reclaim it and find it was never declared.
What makes it the sharpest arm in this pass is the figure that did not move. Because prices are quoted inclusive, the charged amount stayed at exactly 1000 euro. The customer paid what the site said. The invoice total is correct. The card statement is correct. The only thing that changed is the split: net fell from 1000 to 813.01, and 186.99 was reallocated from the seller's revenue to a tax line that will never be paid to anybody.
Nothing in the customer-facing world reports this. It surfaces at reconciliation, if the reconciliation compares the tax line against a jurisdiction rule rather than against itself, and otherwise at an audit.
A scope that reverts to everywhere
Renaming jurisdictions removes the entire rate table. The engine does not then refuse to calculate; it falls back to applying one default rate everywhere, and that default is zero. The reported scope reads (none - one default rate everywhere) and every order on the site is zero-rated.
The invoices remain well-formed. Totals are correct. Customers are charged the price on the shelf and are perfectly happy, because for an inclusive-price storefront the customer's number does not depend on the rate at all. The liability accrues silently, on every order, in every country, until somebody asks why the VAT return is empty.
Renaming rate on the jurisdiction rows reaches the same silence by the NaN route. The reported rates read US-CA=NaN% GB=NaN% DE=NaN%, the charged figure stays exactly right, and the tax and net figures become NaN. A number that is uncomparable rather than wrong is the one a threshold-based alert will not catch, and NaN in a currency field prints without complaint into every downstream report.
The loud ones, and the one-cent one
Not everything here is quiet. Renaming pricesIncludeTax reverts the engine to adding tax on top, so a 120 pound shelf price is charged at 144. Customers notice that inside a day, and in several jurisdictions it is also a consumer-law problem rather than merely an accounting one. Renaming exemptCustomers starts charging the tax-exempt school. Both are recoverable because both are visible.
The most interesting small one is rounding. The application supplies its own half-to-even function because the invoicing system uses it, and a tax engine that disagrees produces a penny of drift on every reconciliation run. Renaming that option does not switch rounding off -- the library substitutes its own half-up implementation, which is a perfectly correct rounding rule that simply is not yours. The measured order moved from 12.34 to 12.35. One cent, on the figure that is remitted, on every invoice that lands on a half cent.
That is the failure shape where the guard's own telemetry stays reassuring: rounding is configured, rounding is running, rounding is correct. It is just not the rounding you agreed with your accountants.
What protection alone did
Nothing. Five profiles, byte-identical output, every charge and every tax figure identical to the cent. The tax engine is not disturbed by renaming identifiers, encoding strings or compressing the bundle.
As everywhere in this series, the results above required member renaming aimed at names an installed library reads.
What to do about it
Do not point a renaming pattern at a tax configuration or at the rule rows inside it. The rows are the easy ones to miss, because a jurisdiction table reads as data rather than as configuration -- and a single boolean on one row was the most expensive arm measured this pass.
Calculate tax server-side. This is the one area in this series where the recommendation is unconditional: the client is the wrong place for a figure that a revenue authority will later ask you to justify, and every failure in this article disappears if the browser is not the thing computing it.
Then reconcile against the rules rather than against the ledger. A test that asserts an Irish B2B order carries zero tax, that a UK inclusive order splits 120 into 100 and 20, and that a rate table is present and finite, catches every result in this article on the first build.
Frequently asked questions
Does obfuscation change the tax a storefront charges?
Not by itself. Protected on five profiles, the sample produced identical charges, tax figures and net figures to the cent. The results in this article required member renaming aimed at the names the tax engine reads.
What was the most expensive single rename?
The reverseCharge flag on one jurisdiction row. It put 186.99 euro of VAT on a 1000 euro business-to-business order -- tax that is not owed, cannot be remitted, and came out of the seller's own revenue.
Why would nobody notice that one?
Because the storefront quotes tax-inclusive prices, so the amount charged did not change at all. The customer paid the advertised 1000 euro and the invoice total was correct. Only the split between net and tax moved.
What happens if the whole jurisdiction table is renamed?
The engine falls back to one default rate applied everywhere, and that default is zero. Every order on the site is zero-rated, invoices stay well-formed, and the liability accrues quietly until the VAT return is questioned.
Can a renamed rate make the tax figure NaN?
Yes. The rates printed as NaN percent and the tax and net figures became NaN, while the charged amount stayed exactly correct. A currency field holding NaN prints without complaint into downstream reports and will not trip a threshold-based alert.
What does renaming the rounding function do?
The library substitutes its own half-up rounding for your half-to-even. The measured tax moved from 12.34 to 12.35 -- one cent, on the remitted figure, on every invoice that lands on a half cent. Rounding still reports as configured and running.
What is the right architectural answer?
Calculate tax on the server. It is the one recommendation in this series that does not depend on how you configure protection, because a figure a revenue authority may ask you to justify should not be computed in a bundle you ship to customers.
Related reading