Announcement: You can find the guides for Commerce 7.5 and later on the new Elastic Path Documentation site. The Developer Center continues to support Commerce 6.13.0 through 7.4.1.Visit new site

Tax calculation API

Tax calculation API

Elastic Path has a capabilities-based SPI interface for supporting tax calculation plugins, defined in the /commerce-engine/tax-calculation-connectivity module. The default tax calculation plugin, defined in the com.elasticpath.service.tax package, is contributed through this interface.

To implement a new tax calculation plugin, extend AbstractTaxProviderPluginSPI and implement one or more of the following capabilities:

Capability Methods Defined Description
TaxCalculationCapability TaxedItemContainer calculate(TaxableItemContainer container)

Implementation required for all tax plugins. Provides a method which calculates taxes for all items in a TaxableItemContainer object. The results are returned in a TaxedItemContainer object.

TaxExemptionCapability None; this is a marker interface. Implementation required for tax plugins which support tax exemption. An exception is returned if a tax exemption ID is provided to a plugin that does not implement this interface.
StorageCapability

void archive(TaxDocument taxDocument, TaxOperationContext taxOperationContext)

void delete(TaxDocument taxDocument, TaxOperationContext taxOperationContext)
Implementation optional. Provides two methods for tracking taxes on remittance reports:
  • archive(): Records tax data at the time of a cart purchase.
  • delete(): Deletes tax data when an order is returned.

Creating a Tax Plugin

Note: Before creating a custom tax plugin, check the Accelerators repository on code.elasticpath.com to see if an accelerator that you want to use already exists.
  1. Create a plugin project in the /ep-commerce/extensions module.
  2. Create a tax provider plugin class that extends the AbstractTaxProviderPluginSPI class.
  3. Implement the TaxExemptionCapability and TaxCalculationCapability interfaces.
  4. Optional: Implement the StorageCapability interface.
  5. Wire the plugin into the Cortex and Core Commerce modules.

Creating the plugin project

  1. Create a maven project for your plugin in the /ep-commerce/extensions module.

    Elastic Path recommends creating a /ep-commerce/extensions/tax-plugins directory for all tax plugins.

  2. Create a pom.xml file with the following settings, where tax-plugin-custom is the name of your tax plugin module:
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
       <parent>
          <groupId>com.elasticpath.extensions</groupId>
          <artifactId>ext-commerce-engine-parent</artifactId>
          <version>0-SNAPSHOT</version>
       </parent>
       <modelVersion>4.0.0</modelVersion>
     
       <name>Tax Plugins</name>
       <artifactId>ext-tax-plugins-parent</artifactId>
       <packaging>pom</packaging>
     
       <modules>
          <module>tax-plugin-custom</module>
       </modules>
    </project>
  3. Add the /tax-plugins directory as a maven module to the pom.xml file of /ep-commerce/extensions.
  4. In the /tax-plugins directory, create a subdirectory for your custom tax plugin.

    Elastic Path recommends that the directory name matches the name of the module.

  5. In the /tax-plugins/tax-plugin-custom directory, create a pom.xml file with the following:
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
       <parent>
          <groupId>com.elasticpath.extensions</groupId>
          <artifactId>ext-tax-plugins-parent</artifactId>
          <version>0-SNAPSHOT</version>
       </parent>
       <modelVersion>4.0.0</modelVersion>
     
       <name>Custom Tax Calculation Plugin</name>
       <artifactId>tax-plugin-custom</artifactId>
       <packaging>jar</packaging>
     
       <dependencies>
          <dependency>
             <groupId>com.elasticpath</groupId>
             <artifactId>tax-calculation-connectivity-api</artifactId>
          </dependency>
          <dependency>
             <groupId>com.elasticpath</groupId>
             <artifactId>ep-core</artifactId>
          </dependency>
          <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
          </dependency>
          <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
          </dependency>
          <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
          </dependency>
       </dependencies>
     
       <build>
          <plugins>
             <plugin>
                <artifactId>maven-checkstyle-plugin</artifactId>
             </plugin>
             <plugin>
                <artifactId>maven-pmd-plugin</artifactId>
             </plugin>
          </plugins>
       </build>
     
    </project>
  6. In the /tax-plugins/tax-plugin-custom directory, create the standard directory structure for a Java Maven project:
    src/
      main/
        java/
        resources/
      test/
        java/
        resources/

Creating the tax provider plugin class

The plugin contract only requires implementing a TaxProviderPlugin class that extends AbstractTaxProviderPluginSPI and implements the TaxCalculationCapability interface. However, Elastic Path recommends breaking up the functionality into separate collaborator classes that are invoked by the TaxProviderPlugin class, such as a service class to make the actual external call and adapter classes to transform the input and output classes to or from the internal representations required by the tax service.

Create a class as in the following example:

package com.elasticpath.extensions.tax.provider;
 
import org.apache.log4j.Logger;
 
import com.elasticpath.plugin.tax.spi.AbstractTaxProviderPluginSPI;
 
/**
 * Custom tax provider.
 */
public class CustomTaxProviderPlugin extends AbstractTaxProviderPluginSPI {
 
   private static final Logger LOG = Logger.getLogger(CustomTaxProviderPlugin.class);
 
   /**
    * Custom Tax Provider name.
    */
   public static final String PROVIDER_NAME = "CustomTax";
 
   @Override
   public String getName() {
      return PROVIDER_NAME;
   }
   }

The getName() method returns a unique name for this tax provider. The name is recorded in the database as part of the TTAXJOURNAL table, so that the tax provider used to determine the taxes is tracked.

Implementing the plugin interfaces

Implementing TaxCalculationCapability

All tax plugins must implement this interface.

Modify your plugin class to implement the TaxCalculationCapability interface and implement the calculate() method. The calculate() method accepts a TaxableItemContainer object and returns a TaxedItemContainer object.

The TaxedItemContainer object contains the following:

  • A list of TaxedItem objects. A single TaxedItem object contains information about the taxes applied to a specific item in a shopping cart.

A TaxedItem object contains the following:

  • A collection of TaxRecord objects. A single TaxRecord object contains information about one specific tax applied to the TaxedItem object.
The following sample code shows portions of the code that need to be implemented for the calculate() method:
Note: In the example, replace ExternalServiceResult, ExternalServiceTaxedItem, and ExternalServiceTaxedItemBreakdown with any classes returned by your external tax service.
public TaxedItemContainer calculate(final TaxableItemContainer container) {
   // Transform TaxableItemContainer into structure required by external service
 
   // Call external service to calculate tax breakdown for all items
 
   final MutableTaxedItemContainer result = getBeanFactory().getBean(TaxContextIdNames.MUTABLE_TAXED_ITEM_CONTAINER);
   result.initialize(container);
 
   for (ExternalServiceTaxedItem externalServiceTaxedItem : externalServiceResult.getItems()) {
      final MutableTaxedItem taxedItem = getBeanFactory().getBean(TaxContextIdNames.MUTABLE_TAXED_ITEM);
      TaxableItem taxableItem = container.getItems().get(externalServiceTaxedItem.getLineNumber() - 1);
      taxedItem.setTaxableItem(taxableItem);
      BigDecimal priceBeforeTax = taxableItem.getTaxablePrice();
      if (container.isTaxInclusive()) {
         priceBeforeTax = priceBeforeTax.subtract(externalServiceTaxedItem.getTaxAmount());
         taxedItem.addTaxInPrice(externalServiceTaxedItem.getTaxAmount());
      }
      taxedItem.setPriceBeforeTax(priceBeforeTax);
 
      for (ExternalServiceTaxedItemBreakdown externalServiceTaxedItemBreakdown : externalServiceTaxedItem.getBreakdown()) {
         final MutableTaxRecord taxRecord = getBeanFactory().getBean("mutableTaxRecord");
         taxRecord.setTaxProvider(PROVIDER_NAME);
         taxRecord.setTaxValue(externalServiceTaxedItemBreakdown.getTaxAmount());
         taxRecord.setTaxRate(externalServiceTaxedItemBreakdown.getTaxRate());
         taxRecord.setTaxName(externalServiceTaxedItemBreakdown.getTaxName());
         taxRecord.setTaxCode(externalServiceTaxedItemBreakdown.getTaxCode());
         taxRecord.setTaxRegion(parseTaxRegion(externalServiceTaxedItemBreakdown.getTaxProvince()));
         taxRecord.setTaxJurisdiction(externalServiceTaxedItemBreakdown.getTaxCountry());
         taxedItem.addTaxInPrice(externalServiceTaxedItemBreakdown.getTaxAmount());
         taxedItem.addTaxRecord(taxRecord);
      }
       
      result.addTaxedItem(taxedItem);
   }
 
   return result;
   }

Following are the fields of each of the objects that must be populated:

MutableTaxedItemContainer
Field/Method Description
initialize(TaxableItemContainer taxableItemContainer) Copies the origin address, destination address, store code, currency, and tax inclusive flag from the taxable item container.
addTaxedItem(TaxedItem taxedItem) Adds a populated TaxedItem object, representing each line item of the shopping cart.
MutableTaxedItem
Field/Method Description
setTaxableItem(TaxableItem taxableItem) Specifies the taxable item that for the TaxableItemContainer object. The plugin must match each line of the external tax service result to the corresponding TaxableItem object.
addTaxInPrice(BigDecimal amount) Adds tax to the total price for a shopping cart line item.
setPriceBeforeTax(BigDecimal amount) Sets the subtotal amount for the shopping cart line item without taxes, even in tax inclusive regions. This value must not include any discounts that are applied to the shopping cart subtotal. Do not use the taxableItem.getTaxablePrice() method to set this value.
addTaxRecord(TaxRecord taxRecord) Adds a populated TaxRecord object to the collection of tax records for a specific shopping cart line item and recalculates the total taxes on the line item.
MutableTaxRecord
Field/Method Description
setTaxProvider(String taxProvider) Sets the tax provider name.
setTaxCode(String taxCode) Sets the tax code for the tax that is being charged, for example GST or PST in Canada.
setTaxName(String taxName) Sets the human readable name for the tax that is being charged.
setTaxJurisdication(String taxJurisdiction) Sets the country where the tax applies.
setTaxRegion(String taxRegion) Sets the tax region where the tax applies. Usually, a tax region is a province, state, or country.
setTaxRate(BigDecimal taxRate) Sets the tax rate for this tax code. This should be passed as a decimal amount. For example a 7% rate should be passed to the method as 0.07.
setTaxValue(BigDecimal taxValue) Sets the tax value in dollars for this tax code.

Implementing TaxExemptionCapability

If your tax provider supports tax exemption IDs, such as VAT exemption codes in the EU, modify your plugin class so it implements the TaxExemptionCapability interface. This interface is a marker interface with no methods. The value returned by the taxableItemContainer.getTaxOperationContext().getTaxExemption() method is only returned to tax providers implementing the TaxExemptionCapability interface, and is used by the tax provider plugin to identify customers with a tax exemption status.

Implementing StorageCapability

If your tax provider supports tracking of charged taxes for the purposes of tracking remittance, modify your plugin class so it implements StorageCapability. This interface defines the archive() and delete() methods. The archive() method notifies the external tax service of taxes charged to the customer. The delete() method notifies the external tax service of taxes to return to the customer.

Each of these methods is passed a TaxDocument object, which contains details of the taxes charged to the customer.

Wiring a tax plugin into Elastic Path

Wire your plugin into the Cortex and Commerce Manager modules.

Wiring varies for each tax plugin. The following is a general procedure to wire a tax plugin:
  1. Add any maven dependencies of your tax calculation plugin to the /extensions/core/ext-core module's pom.xml file.
  2. Determine a selection strategy for your plugin.

    For more information, see Tax plugin selection strategies.

  3. Add bean definitions, mappings, and define the tax plugin selection strategy in the extensions/core/ext-core/src/main/resources/META-INF/conf/ep-core-plugin.xml file.

Tax plugin selection strategies

Elastic Path provides five selection strategies, as defined by classes, which determine which tax plugin is provided to a user. All strategies fall back to Elastic Path's default tax plugin if an explicit match is not found.
Selection Strategy Class Description
Single Sets a default tax provider for use in all cases.
Store StoreSettingsTaxProviderSelectionStrategy Selects a tax provider by store using Settings definitions.
Store StoreTaxProviderSelectionStrategy Selects a tax provider by store using Spring configuration.
Region (Country) RegionTaxProviderSelectionStrategy Selects a tax provider based on the TaxRegion object, which by default uses the tax address country as the region. This can be extended to use other criteria to determine the tax region.
Composite CompositeTaxProviderSelectionStrategy Selects a tax provider based on the product's tax code. Multiple tax providers can be used to calculate taxes for a single shipment.