Commerce 2.x Stories: Taxes
"Why doesn’t Commerce/Magento/$otherSolution handle my taxes properly? That’s the most basic feature!” - many people, often.
When it comes to eCommerce, nobody likes taxes. We expect taxes to “just work”, so we can finish our projects and get on with our lives. At the same time, no other topic is as complex.
Selling online puts us at the crossroads of different (and sometimes conflicting) laws with many rules and even more exceptions. All eCommerce systems provide the basic tools (“Define your tax rates and specify when to apply them”) and make the site developer responsible for tax compliance. The developer usually passes that responsibility to the client, sometimes implicitly. The client consults an accountant, sometimes. But the buck has to stop somewhere, and it often comes back to the developer, 5 days after launch.
As taxes become more and more complex, there is a need for smarter tax handling, where the application does more and the site administrator less. In the Commerce 1.x lifecycle we’ve built the commerce_vat module to handle the more and more complex VAT taxes. For 2.x, we’re bringing this approach back into core, and releasing several libraries to share the solution with the wider PHP community.
Problems and potential solutions
What are my tax rates?
Each country defines one or more tax rates. There is usually a standard rate which applies to most products, and one or more reduced rates which apply to special products like wine or ebooks.
Most eCommerce systems provide no predefined tax rates. The site administrator / developer must research the rates that must be charged, and enter them into the system. Easy enough, right? If the store operates from Serbia, we create the Serbian tax rate(s), apply them if the customer is from Serbia, call it a day. Unfortunately, most stores operate in countries that make this more complicated.
Canada has 13 subdivisions (10 provinces, 3 territories), each with their own tax rates. If the customer is from Canada, the store must charge the tax rates defined by the customer’s home province/territory. This means that all 13 sets of tax rates must be defined for Canada.
The EU has 28 countries. A French company selling t-shirts will charge French VAT to EU customers. If the French company sells more than 35 000 EUR of t-shirts to Belgium per year, it must start charging Belgian VAT to customers from Belgium. From January 1st 2015, for all digital products and services sold inside the EU (e.g. hosting, ebooks, elearning, memberships) the customer’s VAT rates must be charged. Belgian customer - Belgian VAT. German customer - German VAT. This means that the store must know the tax rates of all 28 EU countries.
Other countries are also trying to impose charging their own taxes when digital products are sold to their residents, for example Norway, South Africa, and soon Japan. While it is currently unclear how much power they have to enforce this, trade agreements will make this a reality and add those rates to the list of ones to know and maintain.
We (the PHP eCommerce community) need to create&maintain a dataset of known tax rates.
We can do this for all countries except the US (where tax rates vary even by zip code, and are therefore impossible to predefine). Users will then be able to simply import the tax rates they need.
We also need to provide resolver classes with territory-specific (EU, Canada...) rules for selecting the right tax rates.
This allows the system to automatically choose the right strategy based on the customer and store location. More on this later (under "Resolving the applicable tax types and rates").
Tax rates can change.
France has 4 different tax rates: Standard, Intermediate, Reduced, Super Reduced. On January 1st 2014, the Standard rate changed from 19.6% to 20%. The Intermediate rate changed from 7% to 10%.
If a store needs to care about several tax rates, chances are that at least one of them will change in a given year. Since eCommerce systems don’t provide a data model to handle this, it means spending New Year’s Eve with one’s finger on the submit button, so that the tax rate percentage is changed at midnight, and all customers pay the correct tax rate.
Each TaxRate object must reference a set of TaxRateAmount objects, each containing a percentage and its start/end dates. The TaxRate should be able to return the correct amount object for a requested date.
This allows the system to reference tax rates without fearing about percentage changes, since the actual percentages are stored one level below.
We can group the TaxRate objects using TaxType objects (French VAT, Australian GST, etc), containing an admin label and data common to all rates (e.g, the rounding mode, other information needed for calculation).
Resolving the applicable tax types and rates
In order to start implementing classes for resolving tax rates, we must first define the tax type “zones”. The zone specifies the territories where the specific tax type and its rates are in use. Common sense tells us that French VAT is used in France, German VAT is used in Germany, etc.
But is French VAT used only in France? No, it is used in Monaco as well, which is a separate country. The UK VAT is also used on the Isle of Mann, a Crown dependency, but not an EU member. German VAT isn’t used on the island of Heligoland and the town of Büsingen (where Swiss VAT is used), but it is used in two Austrian towns: Jungholz and Mittelberg. Austrian VAT is then used in all of Austria except in Jungholz and Mittelberg.
So much for common sense. And these are only some of the examples.
Each TaxType must have a matching Zone specifying the territories where the TaxType is in use.
Once we know where the tax types are used, we can implement our resolvers. Each resolver is a class (tagged service in Symfony, plugin in Drupal) that takes a taxable object, customer and store information, and tries to find the applicable tax types. One resolver evaluates one set of tax types ("EU", "Canada", "All others"), implementing the expected logic, such as:
- In Canada, the store charges the tax rates defined by the customer’s home province/territory. Selling from Quebec to Ontario? Apply the Ontario HST.
- A French company selling physical products (e.g. t-shirts) will charge French VAT to EU customers.
- A French company selling digital products (e.g. ebooks) from January 1st 2015 will apply the customer's tax rates (German customer - German VAT)
- A French company providing a training in Belgium will charge Belgian VAT (explicit place of supply).
Resolvers are registered in a central service, sorted by priority and invoked individually until one returns a result.
Zones are territorial groupings used for shipping or tax purposes. A zone can match other zones, countries, subdivisions (states/provinces/municipalities), postal codes. Postal codes can also be expressed using ranges or regular expressions.
Our new tax library:
- Smart data model designed for fluctuating tax rate amounts ("19% -> 21% on January 1st")
- Predefined tax rates for EU countries and Switzerland. More to come.
- Tax resolvers with logic for all major use cases.
Visit the GitHub pages for code examples and further implementation details.
To summarize, what does this mean for Commerce 2.x?
- Expanded data model with support for fluctuating tax rate amounts
- New concept of zones (reusable for shipping as well)
- Predefined tax rates
- Greatly expanded default logic
We’re tackling pricing next, from storage to calculation, with a special focus on the interaction between discounts and taxes.