Tax Calculation
Elastic Path has a capabilities-based SPI (Service Provider 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:
TaxCalculationCapability
Methods defined:
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 aTaxedItemContainer
object.TaxExemptionCapability
Methods defined: 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
Methods defined:
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 purchasedelete()
: 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.
- Create a plugin project in the
/ep-commerce/extensions
module. - Create a tax provider plugin class that extends the
AbstractTaxProviderPluginSPI
class. - Implement the
TaxExemptionCapability
andTaxCalculationCapability
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 thepom.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 apom.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, we recommend breaking up the functionality into separate collaborator classes that are invoked by the TaxProviderPlugin
class, such as a service class and adapter class. A service class makes the external call. The adapter classes 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
TaxCalculationCapability
Implementing 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 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 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
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
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 correspondingTaxableItem
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
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 (Goods and Services Tax) or PST (Provincial Sales Tax) 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
TaxExemptionCapability
Implementing If your tax provider supports tax exemption IDs, such as VAT (Value Added Tax) 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.
StorageCapability
Implementing 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:
Add any maven dependencies of your tax calculation plugin to the
/extensions/core/ext-core
module’spom.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.
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.
Single Selection Strategy
Sets a default tax provider for use in all cases.
Store Selection Strategy
StoreSettingsTaxProviderSelectionStrategy
Selects a tax provider by store using Settings definitions.
StoreTaxProviderSelectionStrategy
Selects a tax provider by store using Spring configuration
Region (Country) Selection Strategy
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 Selection Strategy
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