Learn how to implement VAT for your e-commerce website with Sylius in 6 minutes
Tags: php, backend, webdev, beginners
Published: 2024-04-24 14:59:32

If you are using Sylius for your e-commerce website, then you will inevitably come across the subject of Value Added Tax (VAT). As a software engineer at [Theodo](https://www.theodo.fr/), I have spent countless hours customizing every one of its aspects, for it to behave in the exact way I wanted it to. 


**So you don't need to! Here are the main steps you can follow to avoid wasting any time!**

**Table of contents**:
- [Setting up your first VAT engine](#setting-up-the-engine) 🔧
    1. [Tax category](#tax-category) 🏷
    2. [Zone](#zone) 🌍
    3. [Tax rate](#tax-rate) 💸
- [How does Sylius VAT engine works?](#understanding-sylius-vat) 🧠
    1. [The Order Processor](#order-processor) ✨
    2. [The Applicators](#applicators) 👨‍💻
    3. [The Calculators](#calculators) 🧮
- [My tips and tricks](#tips-and-tricks) 🥇
   1. [Changing the default address](#changing-default-address) 🏠
   2. [Multiple zones for one country](#multiple-zones-by-country) 🌐
- [Conclusion: customize your website](#conclusion) 🔥

## <h2 id='setting-up-the-engine'>Setting up your first VAT engine 🔧</h2>

Sylius VAT handling revolves around three distinct entities, allowing it to choose which rate it has to apply. One of these entities is linked to the article - **the tax category**, another is linked to the customer - **the zone**, and the last one connects the latter two - **the tax rate**.

![A schema representing the different entities involved in Sylius VAT calculation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9gjuh2eez0k9pb7w2hwm.jpg)

### <h3 id='tax-category'>Tax Category 🏷</h3>

The tax category is just a category for your product, which defines which rates should be applied. **It allows you to sell products which aren’t taxed for the same rate** - for example, clothes and food.
You’ll need to add all of your tax categories to your fixtures (the base data you want to have every time you restart your shop - see [Sylius documentation](https://docs.sylius.com/en/1.12/book/architecture/fixtures.html)):

```yaml
    fixtures:
        tax_category:
            options:
                custom:
                    food:
                        name: 'Food'
                        code: 'food'
                    clothes:
                        name: 'Clothes'
                        code: 'clothes'
```

### <h3 id='zone'>Zone 🌍</h3>

The zone is a Sylius concept which is way broader than the VAT: it is **either a group of countries, a group of provinces, or a group of other zones**. Here, what we will be looking at is the zone of the customer. A zone has an interesting characteristic, called scope, which defines what it can be used for: ‘shipping’, ‘tax’, or ‘all’. A zone with a shipping scope is only used to calculate shipping methods and fees, whereas a zone with tax scope is only used to calculate VAT. If the scope is all, the zone will be used for both.
You’ll need to define the different zones that you need to your fixtures:

```yaml
    fixtures:
        geographical:
            options:
                countries:
                    - 'FR'
                    - 'DE'
                    - 'US'
                    - 'GB'
                zones:
                    EUROPE:
                        name: 'Europe'
                        scope: 'tax'
                        countries:
                            - 'FR'
                            - 'DE'
                    AMERICA:
                        name: 'America'
                        scope: 'tax'
                        countries:
                            - 'US'
```

### <h3 id='tax-rate'>Tax Rate 💸</h3>

Finally, a tax rate is an entity that defines **all the necessary information for Sylius to calculate the amount of taxes** your customer should pay. This must at least contain:
- a tax category: which of the products are concerned
- a zone: which of your customers are concerned
- an amount: how much is the VAT ? For 20%, you have to write 0.2.
- a calculator: how should the VAT be calculated ? Sylius provides a calculator named default that does a basic division, but you can define and provide others if you need to.
- a `isIncludedInPrice` boolean: is the VAT included in the price that you defined initially for the article ?

The last field, `isIncludedInPrice`, might seem enigmatic at first glance, however it is quite simple. It defines how the VAT should be calculated. For example, for a T-shirt costing 12€, and a tax rate of 20%:
- if `isIncludedInPrice` is false, that means that the 12€ was tax free, and the final price is 14.40 € (2.40€ of VAT included)
- is `isIncludedInPrice` is true, that means that the 12€ was tax included, and Sylius will keep track of 2€ of this being taxes, without changing the total amount.

You’ll need to define **a tax rate for every couple of zone and tax category**:

```yaml
    clothes-europe:
        code: 'clothes-europe'
        name: 'Tax rate for clothes in Europe'
        category: 'clothes'
        zone: 'EUROPE'
        amount: 0.2
        calculator: 'default'
        included_in_price: true
    clothes-america:
        code: 'clothes-america'
        name: 'Tax rate for clothes in America'
        category: 'clothes'
        zone: 'AMERICA'
        amount: 0.15
        calculator: 'default'
        included_in_price: true
    food-europe:
        code: 'food-europe'
        name: 'Tax rate for food in Europe'
        category: 'food'
        zone: 'EUROPE'
        amount: 0.2
        calculator: 'default'
        included_in_price: true
```

If you only need to setup a basic VAT engine, without any added complexity, then you’re good to go! Jump to your code editor and try it out! **Sylius will handle all the calculations on its own**, and add the correct adjustments (modifications of price) to your articles automatically.

## <h2 id='understanding-sylius-vat'>How does Sylius VAT engine works? 🧠</h2>

However, **if you need a custom behavior of your engine** (for example: billing a commission and adding VAT to it), or if you want a deeper understanding of this system, I’ll dive further into the engine now.

### <h3 id='order-processor'>The Order Processor ✨</h3>

The VAT engine is included in the OrderProcessor: **a list of operations that are executed every time a cart is modified** (for example, it is triggered by adding an item to cart, changing the shipping address, etc…). That means that the VAT will be erased and recalculated every time your customer modifies its cart. This includes recalculating the shipping fees, the VAT, ensuring that every item is sold at the correct price, etc...

When the OrderProcessor asks for a VAT calculation, it calls a service called OrderTaxesProcessor, whose only goal is to call some Applicators. Applicators are where the magic happens, and are responsible for **finding the correct tax amount**, and **adding this amount to the order**. Each object that can generate VAT has a dedicated Applicator: there is one for the OrderItem, one for the shipping fees, etc…

Note that, prior to asking for a VAT calculation, the OrderProcessor calls another service which **removes the taxes from the order**. That allows us to calculate the VAT again, from an empty and controlled state.

![A schema of the different steps of the Order Processor used by Sylius](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvhqae7fqkrha1qq17ul.png)

*Schema of the different steps of the Order Processor from [Sylius documentation](https://docs.sylius.com/en/1.12/)*

### <h3 id='applicators'>The Applicators 👨‍💻</h3>

An applicator will do the following steps:
- **Find the zone** which corresponds to the current cart. If the order has a billing address, it uses it to find the zone. Otherwise, it uses the default zone of your channel.
- **Find the tax category** of every item in the cart.
- For each item, **look for a tax rate** that has both the correct tax category and zone.
- If a tax rate is found, **adding VAT adjustments** with the correct rate, otherwise do nothing.

### <h3 id='calculators'>The Calculators 🧮</h3>

The Sylius VAT calculators are some pieces of code that handle **the calculation of the correct tax amounts**. They are given a price and a tax rate, and they return the correct amount of vat. Sylius, by default, provides one that calculates the VAT the way you expect it (a basic division), but if you need to implement some custom logic here (for example, no VAT at all for product that cost less than 5€), you can do so easily.

The only things to do are:
- **Create a service** which implements the Sylius CalculatorInterface with your own logic
- **Register it in your services** with the dedicated tag and the name you want to use in your fixtures.

```php
    // NoTaxesIfPriceLowerThanFiveCalculator.php
    class NoTaxesIfPriceLowerThanFiveCalculator implements CalculatorInterface
    {
        public function calculate(float $base, TaxRateInterface $rate): float
        {
            if ($base <= 5) {
                return 0;
            }

            if ($rate->isIncludedInPrice()) {
                return round($base - $base / (1 + $rate->getAmount()));
            }

            return round($base * $rate->getAmount());
        }
    }
```

```php
    // services.yaml
    App\Taxation\Calculator\NoTaxesIfPriceLowerThanFiveCalculator:
        tags:
            - { name: 'sylius.tax_calculator', calculator: 'no_taxes_if_price_lower_than_five' }
```

## <h2 id='tips-and-tricks'>My tips and tricks 🥇</h2>

Now you know almost everything that you need to customize and code tax calculations with Sylius with ease. However, there is still **some details and tricks** that I discovered which can be useful to anyone working with this system.

### <h3 id='changing-default-address'>Changing the default address 🏠</h3>

By default, to calculate the zone which corresponds to the current cart, Sylius uses the billing address. However, if you want it to **use the shipping address** instead, you don't have to add any code or to overwrite anything. There is an optional parameter that you can add to your yaml configuration, and which does exactly that:

```yaml
    sylius_core:
        shipping_address_based_taxation: true
```

### <h3 id='multiple-zones-by-country'>Multiple zones for one country 🌐</h3>

If a country is attached to multiple zones having 'tax' as their scope, then issues may arise. Indeed, **Sylius expects a country to have a single zone** available for tax, and, if it finds more that one, it will **randomly return one of them**. This will leave you with unexpected behaviors, which can cause severe bugs. Instead, you should split your countries into multiple zones, until no country is present multiple times.

Example: I deliver food and clothes in FR, IE (Ireland) and UK. Food has a 10% rate in every country, but clothes are only taxed in FR with 15% rate. I would like to create a zone containing every country (EUROPE), and a zone containing only FR (FRANCE), so that I can create two tax rates:
- one for EUROPE, food, with a rate of 10%
- the other for FRANCE, clothes, and 15%.

However, this won't work, as France is included in two different zones. Instead, I have to declare a zone containing IE and UK (GREAT-BRITAIN) and a zone containing FR (FRANCE), and then create 3 tax rates:
- one for GREAT-BRITAIN, food, with a rate of 10%
- one for FRANCE, food, with 10%
- and one for FRANCE, clothes, and 15%

## <h2 id='conclusion'>Conclusion: customize your website 🔥</h2>

You can now easily twist the Sylius VAT engine to your needs, and explore its features in detail. And the best part is that you can replace any part of this engine! Thanks to the Sylius architecture using numerous specialized services, you can easily override and replace any code that doesn't exactly meet your needs, allowing you to create the e-commerce website that exactly fits your business.