Avalara Had the Tax Right. NetSuite Was Putting It in the Wrong Bucket.
A fuel distributor selling into more than twenty states pays excise tax to every one of them, and every one of them wants its own return, on its own form, with its own definitions of what counts. We had Avalara wired into NetSuite for years to handle that, and it did. Every invoice carried the right tax lines for the right state. If you had asked me whether our excise tax was automated, I would have said yes.
The accounting team would have laughed. The calculation was automated. What happened to those numbers after the invoice saved was a serious chunk of every month, done by hand.
The engine was right and the ledger was blind
Here is what was actually going on. Avalara returned each tax line with everything you would want to know: which state, what kind of tax, how much. NetSuite then took every one of those lines and posted it to one of two general tax accounts, deferred or non-deferred. That was the whole story in the general ledger. We owed tax. We did not, at the account level, know to whom.
So each period, accounting rebuilt the answer from scratch. Pull the raw transactions. Sort them by state and by tax type. Assemble each state’s return by hand. Reconcile Avalara’s reports against the ledger, state by state. Move balances out of the general account into state-specific accounts by journal entry. Then chase down whatever had landed in the wrong bucket or the wrong period and fix it after the fact.
None of this showed up as a problem anywhere in the system. Invoices were right. Customers were billed correctly. The states got paid. The cost lived entirely in people’s hours, which is the kind of cost a system never reports on itself.
A few fuel terms, for everyone else
If you do not work in fuel, three things make this harder than it sounds.
- Deferred versus non-deferred. Non-deferred tax is collected on the invoice and remitted with the return, the way you would expect. Deferred tax is owed later and is not collected on the invoice, so it has to sit in its own account or it contaminates the number you think you are about to remit.
- Fees that are not taxes. Some lines that ride along with the tax are really fees, and fees belong in cost of goods, not in a tax liability. Treat them as tax and both your margin and your return are wrong.
- Allowances. Some states reduce what you owe under certain conditions. Missouri has one. An allowance is a reduction in tax, which means it has to be tracked as its own thing, or the ledger overstates what is due.
Two general accounts cannot hold any of that. A state-specific structure can, but only if every line knows where to go.
The fix was a table, and a script that reads it
We did not replace the tax engine. Avalara was doing its job. What we built was a set of scripts inside NetSuite that read what Avalara returns on each transaction and post each line to the right account, and the intelligence in those scripts is a mapping table.
Each row in that table is a combination: the state, whether the tax is deferred or not, the product, whether the line is a tax or a fee, and whether an allowance applies. Each combination points at a specific general ledger account. There are more than a thousand rows. Some of the rate data underneath arrives through Boomi from outside; the scripts decide where each line lands.
That is the entire mechanism. Nothing about it is clever. It is the accounting design the ledger should have had from the start, written down once and enforced on every transaction instead of reconstructed by hand every month.
| Each month | Before | After |
|---|---|---|
| Where a tax line posts | One of two general accounts | The state-specific account for its state, type, product, and fee status |
| Building state returns | Sorted from raw transactions by hand | Read from the accounts they already sit in |
| Reconciling Avalara to the ledger | State by state, manually | Ties by construction, exceptions only |
| Reclassifying by journal entry | Every period | Gone |
| Fixing misposted tax | After the fact | Reviewed as exceptions before close |
The two hard parts
The first was getting the table right the first time. Every state has its own rules, and there is no shortcut to enumerating every combination and checking each one against what the state actually says. A wrong row does not throw an error. It posts quietly to the wrong account and surfaces weeks later as a return that does not tie. So the table got built slowly and checked row by row, which is tedious in exactly the way that matters.
The second was trust. Finance had been building these numbers by hand for a long time, and a script that claimed to do it for them had to earn its place. The new postings ran alongside the old process until the two agreed, and then again, before anyone stopped doing it the old way. I would rather spend an extra period proving a number than spend a year being asked to.
What it came to
Measured in hours per month, before against after, the manual excise-tax work dropped by 83%. The tax close got faster. The people who used to rebuild the ledger each period now review exceptions instead, which is what they should have been doing all along.
If you are sitting on a similar setup, and a lot of companies with a tax engine are, here is the short version of what to check.
- Ask where each tax line posts, not just whether it calculates. Those are different questions.
- Enumerate state, tax type, product, and fee status before anyone writes a script. The table is the work.
- Separate fees from taxes, and give allowances their own line.
- Run the new postings alongside the old process until they tie, then run one more period.
- Leave an exception path so the first unmapped combination is a review item and not a surprise.
This is the same kind of integration debt I keep writing about, just wearing an accounting hat. The vendor did its part. The gap was in the design of what happened next, and nobody else was going to close it. It is the sort of problem that sits at the center of what I do: the system was working, and the process around it was not, and the fix was to make them agree.