Rebuilding EU VAT: The Decisions Behind FluentCart’s Tax Module

Most of FluentCart 1.4.0 started as an argument in a chat channel.
Not a fight. A long, careful disagreement about how a store should handle tax when the rules stop being simple. Two people outside our core team drove a lot of it.
By the time we shipped, the shape of the release had changed because of that thread. This post is the part you do not usually see: the decisions we made for EU VAT for WordPress, the ones we argued about, and the ones we chose to put off.
If you only want the outcome, the 1.4.0 release note covers it. If you want to know why the tax engine looks the way it does now, and the people behind it, keep reading.
The Reframe: A price toggle was never the real problem

We started where most stores start. People asked for a switch to show prices with or without VAT. Simple request. We almost built exactly that.
Then Juraj HudƔk, who runs Plexcore and builds on WordPress for EU clients, pushed back in detail. His point was blunt: a toggle solves the wrong problem. A customer does not want to flip a switch. They arrive with a context. A consumer needs the final price with VAT. A VAT-registered business needs the price without VAT, because it can reclaim the tax. One with no VAT registration needs the final price too, because it cannot reclaim anything. The right number depends on who is buying and from where, not on a button.
Jorge de los Reyes MartĆnez, working from the EU side as well, took the same idea and pulled it toward the checkout. He framed the rule we ended up building around: one checkout, one active display mode. A cart should never show one product with VAT and the next product without it. The math underneath can be complex. What the customer reads should be calm and consistent.
That reframe killed the toggle and gave us a real brief. Tax display is about customer context. Tax calculation is a separate problem underneath it.
Once we split those two ideas, the rest of the work got clearer.
Decision 1: The store’s VAT status comes first

The first thing the system now decides is whether the merchant is a VAT payer at all.
This sounds obvious. It was not how the early design worked. We had been treating tax-inclusive and tax-exclusive pricing as a setting any store could toggle. Juraj argued that a merchant who is not VAT-registered should never see those options. They enter final prices. There is no VAT to break down, no inclusive-versus-exclusive choice to make, and showing one only invites mistakes.
For a VAT-registered merchant, he made a second call that shaped the data model: store product prices excluding VAT by default. That is the reliable base for margin math, accounting, imports, exports, and B2B pricing.
Tax-included input can exist, but as an advanced mode, and only if the system records which mode the price came from and keeps a normalized value alongside it. Otherwise an import or an export later has no idea whether a number already contains tax.
So the order of operations became: merchant VAT status, then pricing model, then customer context, then the final calculation at checkout. That order is now baked in rather than assumed.
Decision 2: One display mode, and the Override fight
The hardest debate was about overrides.
Jorge floated a useful case. A mostly B2C store might sell one B2B advisory service through a direct checkout link, where a price without VAT is the correct thing to show. A mostly B2B store might sell a consumer course where a VAT-included price reads better. Real stores are hybrids. He wanted product-level flexibility for those edge cases.
Juraj worried about exactly that flexibility. Uncontrolled product-level overrides can turn a normal cart into a mess, with each line following a different rule. We have all seen a checkout where the numbers stop agreeing with each other. That is the kind of confusion that loses a sale at the last step.
The resolution took a few rounds, and it is worth stating plainly because it changed what we built. We separated two concepts that had been tangled together:
- Product level: tax category and tax profile. A product can be a service, an ebook, a digital download, or a reduced-rate item, because that affects the rate it carries.
- Checkout level: display mode. The cart resolves to one active mode, VAT included or VAT excluded, for the whole order.
An override applies to a flow or a customer context, not to a single line inside a shared cart. A direct-checkout page can run in its own mode. The general shopping cart stays consistent. Our team built a working HTML prototype to test this, and an early version showed multiple VAT rates stacked on one product line. It looked precise. It read as chaos. We cut it. One product line now presents one clear tax treatment, and how a product is priced drives the rate, not a per-line display switch.
Decision 3: Store the decision, not just the Number
This is the change you cannot see and the one we are proudest of.
Old tax handling treats VAT as a number at the bottom of an order. You calculate it, you print it, you move on. That works until someone asks you to prove it. A tax authority, an accountant, an audit, a refund six months later. At that point a number with no story behind it is a liability.
Juraj wrote a long specification for this, north of two hundred fields and data points. The core idea: the order should store the tax decision and the reason for it, not only the result. Was this a B2C sale or B2B? Did reverse charge apply, and on what evidence? Was the VAT ID validated, and when? Which country’s rate, and why that country? When an order carries its own reasoning, every document it produces later, the receipt, the invoice, an export to accounting, can be generated from one trustworthy source instead of reassembled by hand.
That decision had a deadline behind it too. Slovakia, where Juraj works, moves to mandatory electronic invoicing in 2027, and the rest of the EU is heading the same way. You cannot bolt structured e-invoicing onto a system that only remembers totals. You have to record the structured facts at the moment of sale. So even though full e-invoicing is not in this release, 1.4.0 lays the groundwork by changing what an order remembers. That is also why a unified tax summary now renders everywhere from the same component. The thank-you page, the email, and the PDF cannot disagree, because they read the same record.
Decision 4: No paid dependency for VAT rates
While we were arguing about display, Jorge went and solved a quieter problem.
VAT rates change, and there are dozens of them across the EU, with reduced rates for things like ebooks and services. The lazy fix is to pay a third-party API and forget about it. Jorge did not want a paid dependency for data that is already public. He went looking and came back with the European Commission’s tax database, TEDB, which exposes VAT rates by member state, date, category, and product code, plus open datasets built on the same public source.
He also drew a clean line on privacy that we kept. Reading public rate data locally is low risk, because no customer data leaves your store. Validating a VAT number is different. If you route a buyer’s VAT ID and IP through a third-party validator, that data becomes part of someone else’s flow. Validating directly against the EU’s own VIES service is cleaner for GDPR. For a product that sells hard on data ownership, that reasoning fit, and it is now the approach for reverse-charge checks.
Juraj added one caveat we are still carrying: TEDB only covers EU member states, not every European country. A single global source of truth would be ideal, and the commercial ones exist, but the open EU and country-level sources are the better fit for now. We agreed, and that tradeoff is documented rather than hidden.
What We Chose not to Ship Yet
Build-in-public only means something if you are honest about the gaps.
A lot of Juraj’s specification is not in 1.4.0. Full Peppol and UBL e-invoice output is designed but not shipped. Structured export documents for non-EU sales, customs codes, and EORI handling are scoped and waiting. Automated OSS and IOSS registration logic is on the map, not in the build. We could have rushed a thinner version of all of it. But chose to get the foundation right first, because the foundation is the part that is expensive to change later.
We also leaned on prior art and were open about its limits. Jorge recorded a walkthrough of how ThriveCart handles VAT product setup, and it has genuinely good ideas for choosing a product category so the right rate applies. It also has a real gap: its checkout lacks fields that an invoice legally needs. The plan was never to copy a tool. It was to take the good parts, drop the bad ones, and fit them to a self-hosted model where the store owner keeps the data.
What landed in 1.4.0
Here is where the decisions show up in the product you can install today.
Tax now breaks down per item and per rate on every surface: checkout, thank-you page, browser receipt, email, PDF receipt, the admin order view, and the customer’s account.
Reverse charge works for B2B sales to VAT-registered EU buyers, with a price mode that keeps tax-inclusive stores showing the right adjusted total, plus a clear B2B badge on the order. You can choose which EU countries you collect VAT in, register your details inline, and the system warns you when a VAT ID is missing instead of letting the gap surface later.
New stores no longer start blank. FluentCart ships with built-in rates across Africa, the Americas, Asia, Europe, and Oceania. Northern Ireland is treated as its own jurisdiction, and a guided tax step sits inside onboarding. Mixed carts total correctly. Tax overrides can target a city or postcode. A product variation can override tax inclusion. Creating or editing an order in the admin calculates tax on save.
For developers, tax validation, gateway notifications, and several store moments are adjustable through filters, so you can change behavior without forking core. Seller identity and tax ID now appear on PDF receipts, pulled from store settings.
There is a long tail of reliability work underneath, the kind that never makes a headline. Mollie now handles tax correctly on mixed carts. Subscription billing uses each subscription’s own currency. The quieter fixes matter, because correct-once is not the same as correct-every-time.
Straight to production?
We launched 1.4.0 to production without a separate public beta. That is not how we usually treat a change this deep, and it is worth saying why we were comfortable doing it here. The short answer: the hardest parts were already tested before a single line of the final code went in.
Juraj HudĆ”k and Jorge de los Reyes MartĆnez did more than send feedback. They tested. Juraj wrote the two-hundred-field specification, then checked it against real Slovak invoices he supplied from live stores, so the data we record maps to what a tax authority expects rather than what we assumed. Jorge built a working checkout prototype and ran it through the scenarios that quietly break tax engines: domestic VAT, cross-border B2C, reverse charge with a valid VAT ID, exports, and carts that mix tax-inclusive and tax-exclusive lines. When a design failed, it failed in his prototype, on a screen share, weeks before it could fail on someone’s storefront.
That is the difference their work made. Most tax bugs are not coding mistakes. They are design mistakes that only show up in a specific country, for a specific buyer type, in a specific cart. Those are exactly the cases two people running EU stores thought to throw at us.
We cut the confusing multi-rate product line in the prototype. The mixed-cart total got resolved on paper. The reverse-charge display got argued to a clear rule before it shipped. By the time the feature reached our own QA, the edge cases were known quantities, not surprises waiting in production.
So the launch decision was an easy one. The design had been pressure-tested by people who sell into the EU for a living, the data model was validated against real documents, and our internal reliability pass handled the rest. A public beta would have re-discovered problems that were already found and fixed. Their feedback and testing are the reason the module is sharper, and the reason we trusted it in production on day one.
The amount of work and good will that these people have put into this journey is beyond any acknowledgement we can offer. The best wishes and utmost appreciation for you guys! It would literally be impossible without your contributions.

Juraj HudƔk
Plexcore

Jorge de los Reyes M.
En Busca del Fuego
Where this goes next
EU VAT for WordPress is not a feature you finish. It is a moving target, and 1.4.0 is the release where we stopped patching around it and rebuilt the base.
The next stretch is the structured-document layer: invoices, Peppol output, export and customs data, and OSS automation, all reading from the order record we rebuilt. We are building it the same way, in the open, with the people who run EU stores every day.
Thank you to Juraj HudĆ”k and Jorge de los Reyes MartĆnez, whose arguments are in the bones of this thing. If you want to see where it stands today, take a look at what FluentCart does as a commerce plugin, update to 1.4.0, and tell us where your tax setup still hurts. That feedback is how the last release got built, and it is how the next one will.
I’m Jewel, founder of FluentCart and CEO at WPManageNinja, the team behind Fluent Forms, Fluent CRM, Fluent Support, FluentLogs and a handful of other WordPress plugins. I have been writing WordPress code since 2009 and still think of myself as a developer first and an entrepreneur second. Most of what I write on this blog comes from arguments we have had inside the team about how to build software people can actually depend on.

Subscribe now






Leave a Reply