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:
Creating a Tax Plugin
- Create a plugin project in the /ep-commerce/extensions module.
- Create a tax provider plugin class that extends the AbstractTaxProviderPluginSPI class.
- Implement the TaxExemptionCapability and TaxCalculationCapability interfaces.
- Optional: Implement the StorageCapability interface.
- Wire the plugin into the Cortex and Core Commerce modules.
Creating the plugin project
- 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.
- 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>
- Add the /tax-plugins directory as a maven module to the pom.xml file of /ep-commerce/extensions.
- 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.
- 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>
- 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.
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.
- Add any maven dependencies of your tax calculation plugin to the /extensions/core/ext-core module's pom.xml file.
- Determine a selection strategy for your plugin.
For more information, see Tax plugin selection strategies.
- 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
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. |