Apply a VAT rate on a product with Drupal Commerce 2

A calculator

Drupal Commerce 2 now allows you to manage the various taxes and VAT to apply to an online store, regardless of its country and their respective rules in this area. Most of the contributed modules managing these elements on Commerce 1.x are therefore no longer necessary. Let's find out how to use the Drupal Commerce 2.x Resolver concept to set the VAT rate to apply to different products.

Creating a Tax Type

Before determining which VAT rate we wish to apply to this product or that product type, we must first configure the tax type that will be available for our online store.

Drupal commerce now includes in its core all the management and tax detection stuff. And it also includes taxes types already set, such as the (complex) European Union VAT, including the different rates applicable per country.

Drupal commerce TVA en France

 

Thus the addition of a tax is done in a few clicks. Just create a tax type, and select the Plugin corresponding to the European Union VAT (in our case), and voila.

Création d'un type de taxe

 

And to determine the VAT rates applicable to the online store, simply set your online store by specifying for which country it will apply the tax rules.

Drupal commerce store tax settings

Here by specifying France, the VAT rates that will be automatically associated with the online store will be those of the same country. After this very brief introduction to the initial setting up of a Drupal Commerce 2.x online store, let us come to the heart of the subject namely determining the VAT rate to apply depending on the type of product.

The concept of Drupal Commerce 2 Resolver

Drupal Commerce 2.x uses the concept of Resolver, which is neither more nor less than service collectors. To dynamically determine (and also very easily alterable) the tax rate applicable to a product, the order type corresponding to a product, the checkout flow type corresponding to an order, the price corresponding to a product, etc.

In short for each of these actions / reactions we have a service collector that will collect all the services of a certain tag, ordered by priority and then test them one by one, until a service returns a value. And if simply no service is present (or does not return value), then the Resolver implemented by default by Drupal commerce core will play its role.

Thus each declared Resolver service must implement a mandatory resolve() method, which the service collector (or ChainTaxResolver) will evaluate.

/**
 * {@inheritdoc}
 */
public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
  $result = NULL;
  foreach ($this->resolvers as $resolver) {
    $result = $resolver->resolve($zone, $order_item, $customer_profile);
    if ($result) {
      break;
    }
  }

  return $result;
}

And as far as the VAT rate is concerned, Drupal Commerce implements a default Tax Resolver by means of this below service whose priority is set to -100.

services:
  commerce_tax.default_tax_rate_resolver:
    class: Drupal\commerce_tax\Resolver\DefaultTaxRateResolver
    tags:
      - { name: commerce_tax.tax_rate_resolver, priority: -100 }

This service determines the tax rate fairly simply.

/**
 * Returns the tax zone's default tax rate.
 */
class DefaultTaxRateResolver implements TaxRateResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
    $rates = $zone->getRates();
    // Take the default rate, or fallback to the first rate.
    $resolved_rate = reset($rates);
    foreach ($rates as $rate) {
      if ($rate->isDefault()) {
        $resolved_rate = $rate;
        break;
      }
    }
    return $resolved_rate;
  }

}

It simply returns a tax rate that is set by default by the corresponding Plugin, or if no rate is set by default, simply the first rate of those corresponding to the tax zone.

This is a basic rule, which can very easily be sufficient for an e-commerce solution that only sells products whose VAT is the default of the country concerned.

But if several VAT rates are eligible according to the product (reduced VAT rate for services, VAT rate corresponding to cultural products, etc.), then we can very easily determine which VAT rate to apply according to rules that can rely on any product's attributes, or even the customer profile.

Determine a VAT rate with Drupal Commerce 2

To vary an applicable VAT rate according to a product, or a product type, we just need to implement a service that will declare the trade_tax.tax_rate_resolver tag with a priority at least higher than the default TaxResolver provided by Drupal Commerce.

Declare this service.

services:
  my_module.product_tax_resolver:
    class: Drupal\my_module\Resolver\ProductTaxResolver
    tags:
      - { name: commerce_tax.tax_rate_resolver, priority: 100 }

We give it a priority of 100 so that it is evaluated before the default Resolver.

For then, we can evaluate the tax rate to be applied using the resolve() method.

<?php

namespace Drupal\my_module\Resolver;

use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_tax\Resolver\TaxRateResolverInterface;
use Drupal\commerce_tax\TaxZone;
use Drupal\profile\Entity\ProfileInterface;

/**
 * Returns the tax zone's default tax rate.
 */
class ProductTaxResolver implements TaxRateResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
    $rates = $zone->getRates();

    // Get the purchased entity.
    $item = $order_item->getPurchasedEntity();
    
    // Get the corresponding product.
    $product = $item->getProduct();

    $product_type = $product->bundle();

    // Take a rate depending on the product type.
    switch ($product_type) {
      case 'book':
        $rate_id = 'reduced';
        break;
      default:
        // The rate for other product type can be resolved using the default tax
        // rate resolver.
        return NULL;
    }

    foreach ($rates as $rate) {
      if ($rate->getId() == $rate_id) {
        return $rate;
      }
    }

    // If no rate has been found, let's others resolvers try to get it.
    return NULL;
  }

}

In this example, we simply check the associated product type, and if it is a book product (a book eligible for the 5.5% VAT rate), then we simply return the corresponding VAT rate. And for all other products, eligible for the default VAT rate, we let the default Resolver do its work.

We can see here that we could have just as well evaluated a VAT rate based for example on an attribute of the product, and not globally for a product type. This service collector based approach allows us to implement business rules that can be complex in a few lines in an extremely simple and modular way.

To your Resolver !

As mentioned above, Resolver are used extensively on Drupal Commerce 2.x. You want to vary the checkout flow according to the products of an order. Take a Resolver. You want to calculate the price of a product dynamically, according to specific business rules, draw another Resolver. You want to dynamically vary the order type by product, yet another Resolver.

The Resolver will open perspectives to see contributed modules blooming whose primary purpose will be to offer a configuration interface on predefined business rules. But we can now implement a Resolver very simply for business rules that can be very complex, and without having to leave the heavy artillery.

Fire! 

 

Ajouter un commentaire