A WooCommerce store can have a perfect tax table and still charge customers the wrong sales tax. We ran into exactly that on a client’s store recently: a US parts retailer with two physical locations in California was overcharging every customer who chose to collect an order at the second location. The amounts were small, but in sales tax, small and wrong is still wrong.
The client’s question was simple: if a customer selects the shipping method for our second location, is the tax at checkout correct? The store offers standard shipping, pickup and installation at the main shop, and a ship-to-shop option at the second location. Each California city has its own combined rate, so the two locations do not share one. The main shop sits in a city that charges 11%, whereas the second location’s city charges 10.25%.
No, it was not correct. Orders for the second location were taxed at the main shop’s 11% instead of 10.25%. On a $3,399 part, that meant $373.89 in tax instead of $348.40, a $25.49 overcharge. The tax table was accurate; the problem was that WooCommerce never looked up the right row. Therefore, the fix was a small piece of code that tells WooCommerce which location to tax when that specific shipping method is selected, and nothing in the rate data had to change.
WooCommerce decides which rate to apply by picking one taxable address per order. For shipped orders, that is normally the customer’s shipping address, which is correct because the sale happens where the goods are delivered. For pickup orders, however, WooCommerce swaps in the store’s base address, since the customer takes possession at the shop.
That logic works well for a store with one location. The trouble starts with a second one, because WooCommerce only knows a single base address. As a result, every pickup-style method is treated as a sale at the main shop, no matter where the customer actually collects the order. In this case, the second location’s method inherited the main shop’s ZIP code and, with it, the wrong rate.
Timing made this case more interesting. California lets cities and counties add district taxes on top of the statewide 7.25% base, and those rates change on a quarterly schedule. On October 1, a voter-approved, county-wide increase of half a percentage point took effect in the main shop’s county. Consequently, the main shop’s combined rate moved from 10.5% to 11%, whereas the second location, which sits in a different county, stayed at 10.25%.
That update affected our work in two ways. First, it nearly sent us in the wrong direction. Our initial assumption was that 11% was wrong everywhere, because 10.5% had been the correct rate for most of the year. Checking the state’s official rate list, instead of our memory, showed that the store’s table had already been updated and that the main shop’s methods were right. Second, it widened the gap between the two locations from a quarter point to three quarters of a point, which tripled the overcharge on every order sent to the second location.
The update also shaped the design of the fix. Because rates will keep changing, we did not hardcode a percentage anywhere. Instead, the code only tells WooCommerce which ZIP code to use, and the rate itself still comes from the tax table. As a result, the next quarterly update needs a new table import and no code change.
We started by doing the arithmetic on live checkout totals instead of trusting the labels. Dividing the tax by the subtotal gave exactly 11% on every pickup and install method, including the one that should have been lower. Even a test order with an out-of-state address came back at 11%, which confirmed that the customer’s address was being ignored entirely. Next, we exported the store’s tax table, all 2,605 California ZIP rows of it, and checked it for duplicates, conflicting priorities, and stale rates. The table was clean, so the scope narrowed to one shipping method.
The solution uses a WooCommerce filter called woocommerce_customer_taxable_address, which runs after WooCommerce has chosen the taxable address and lets code override it. Our snippet checks the shipping method the customer selected. If it is the second location’s method, identified by its instance ID, the snippet returns that location’s country, state, ZIP code, and city. Otherwise, it returns the address untouched, so every other method behaves exactly as before.
We also added a second, smaller filter for orders edited in the admin area. Without it, a staff member clicking Recalculate on an order would quietly send the tax back to the wrong location.
Implementation was deliberately small. The fix is roughly fifty lines of PHP, packaged as a standalone plugin instead of living in the theme, so a theme update cannot remove it. In addition, the location’s country, state, ZIP code, and city sit in named constants at the top of the file; therefore, a move to a new address means editing one line.
Once the plugin was active, we retested the same cart across the methods. The second location now charges 10.25%, the main shop still charges 11%, and a shipped order to a third California city picks up that city’s own 7.75% rate. After that, we sent the client a short report with the before and after figures and asked them to confirm on their side. Express payment buttons, such as Apple Pay and PayPal, are the last item on the checklist, because they build totals through their own flows.
The fix covers one shipping method today, but the principle behind it is broader: any order that a customer picks up, or has installed, at a physical location should be taxed at that location’s rate, and the address typed into the checkout form should not matter. That applies to every California pickup order and to every install order, at whichever shop performs the work.
For that reason, the natural next step is to replace the single check with a location map. Each pickup or install shipping method gets one entry that pairs its instance ID with the address of the shop behind it. After that, opening a new California location means adding one line to the map, and the existing tax table supplies the correct rate automatically. We would include the main shop in the map as well, instead of relying on the store’s base address, so that every location is handled explicitly and in the same way.
Expansion beyond California follows the same pattern, although it brings three extra tasks. First, the tax table needs rows for the new state, because a table that only contains California ZIP codes has nothing to match an out-of-state shop against. Second, a physical location generally creates a tax obligation in that state, so registration there has to be in place before the first order is collected. Third, the rules themselves differ from state to state, including whether shipping charges and installation labor are taxable. Those are questions for the client’s accountant; once they are answered, the code change is still a single new line in the map.
Tax bugs in WooCommerce are rarely about the rates themselves. More often, the store is looking up the right table with the wrong address, and a rate update or a new location is what finally makes the gap visible. If you run more than one pickup location, test each method with an address from another state, divide the tax by the subtotal, and compare the result to the official rate for the place where the customer actually collects the order. If the numbers do not match, that is the kind of problem we solve at Nucleus, and our work usually falls into four areas.
First, we write and deploy the fix itself. That means mapping each pickup and install shipping method to the address of the shop behind it, packaging the logic as a small standalone plugin, and covering both the checkout and the admin order screen. In addition, we keep the code free of hardcoded rates, so it survives rate changes, theme updates, and new locations without a rewrite.
Next, we look after the rate data that the code depends on. We review the tax table for duplicate rows, conflicting priorities, missing ZIP codes, and stale rates, and we compare it against the official state rate lists. Because California publishes changes on a quarterly schedule, we can also import the updated table at the start of each quarter. Similarly, when a store expands into a new state, we prepare and load that state’s rows before the first order is taken.
After every change, we test instead of assuming. We run the same cart through each shipping method, with in-state and out-of-state addresses, and check every tax amount against the expected rate to the cent. Beyond that, we test the express payment buttons, order recalculation in the admin area, and at least one normally shipped order, to confirm that nothing outside the fix has moved. The client then receives a short report with before and after figures, so their team and their accountant can verify the result independently.
Finally, tax setups drift over time. Plugin updates, new shipping methods, new payment options, and new locations can all reopen a gap that was closed. For that reason, we offer recurring checks as part of our WordPress and WooCommerce maintenance, along with documentation of how the setup works, so the next developer does not have to rediscover it.
All in all, a short review now costs far less than a long conversation with an accountant later, so get in touch if you want us to take a look at your checkout.