Broadleaf Microservices
  • v1.0.0-latest-prod

Tax Common

Overview

TaxCommon is a shared library which provides a single source for tax related domain. This library is used by the Cart Operations Service when calculating taxes during cart pricing. Calculating taxes for pricing purposes is one of the main features of this library. However, some tax calculation Providers, such as Avalara (AvaTax), have facilities to save tax details at or after checkout for the purpose of tax reporting and remittance. This is generally referred to as commit taxes. As a result, there are also post-checkout facilities associated with refunds and cancellations to reverse part or all of the committed taxes, thereby removing them from the taxes that are owed in the tax reporting and remittance system. While calculating taxes happens via the pricing flow in Cart Operations Service, the other tax system interactions typically occur in the Order Operations Service, as this service handles immediate post-checkout activities, refunds, and cancellations.

By default, the TaxCommon library is embedded in various services such as Cart Operations Service and Order Operations Service. In other words, the TaxCommon library is not a stand-alone Microservice. The reason is that this helps avoid additional network hops and serialization/deserialization for high volume pricing activities. For this reason, it is important to ensure that all services that include and use the TaxCommon library maintain the same configurations. If a client wished to create a common Tax Service for use by the Broadleaf Framework or by other 1st or 3rd party systems, that is certainly possible. This would involve creating a new Microservices deployment (e.g. Spring Boot Microservice) with components from the TaxCommon library embedded and configured in that service, and exposed via a custom REST Controller.

Key Components

  • TaxProvider - This interface defines the main interaction with a subsystem whose job it is to calculate and possibly capture sales or VAT taxes for reporting and remittance. Implementors of this interface are often interacting with a distributed system or another service such as Avalara, Vertex, or SimpleTaxProvider (see below). There can be more than one TaxProvider defined and used in a Broadleaf deployment. As a result, it is recommended that TaxProvider Spring Bean definitions be provided a unique bean name. TaxProvider implementations must implement the following methods:

    • String getProviderId() - Returns a constant that uniquely identifies the tax provider (e.g. BLC_SIMPLE_TAX_PROVIDER). It is recommended that implementors do not use the prefex BLC_ for tax provider IDs as this is reserved for tax providers provided by Broadleaf Commerce.

    • <R extends TaxCalculationRequest> boolean canHandle(@NonNull R request, @Nullable ContextInfo contextInfo) - This method is used to determine if this TaxProvider can handle the request. This will typically depend on whether configuration data supports the provided ContextInfo (or null) based on configuration (e.g. if credentials exist for the provided ContextInfo, or for the entire system, regardless of ContextInfo). Implementors may also choose to use the provided TaxCalculationRequest which provides additional data that may influence whether this provider can/should handle the request (e.g. if the provider should be used for taxes in certain countries).

    • TaxCalculationResponse calculateTaxes(TaxCalculationRequest request, @Nullable ContextInfo contextInfo) - Calculates (or estimates) taxes, typically in the context of a Cart pricing flow.

    • CommitTaxResponse commitTaxes(CommitTaxRequest request, @Nullable ContextInfo contextInfo) - Commits taxes, if supported, to a system of record that provides reporting and remittance. Tax Providers that don’t support this should implement a pass-through method that does not throw an exception.

    • TaxCalculationResponse adjustTaxes(AdjustTaxTransactionRequest request, @Nullable ContextInfo contextInfo) - This provides the ability to modify the taxes that have already been committed. This is typically used post-checkout as part of a refund process to reduce the amount of taxes owed in the tax reporting system. Providers that don’t support this should implement a pass-through implementation of this method.

    • ReverseTaxTransactionResponse reverseTaxTransaction(ReverseTaxTransactionRequest request, @Nullable ContextInfo contextInfo) - Similar to adjusting taxes, this allows for the reversal of committed taxes. Providers that don’t support this should implement a pass-through implementation of this method.

  • TaxDelegate - This is a component that is expected to delegate to a TaxProvider. The default implementation is called DefaultTaxDelegate, and contains discovery logic to select the best tax provider for the given request and ContextInfo. A list of Tax Providers are passed to the constructor of the TaxDelegate. The SimpleTaxProvider, if available, is treated as a special case, usually the last Provider to be evaluated during discovery. Additionally, TaxProvider instances have getOrder() method that, by default, returns 0 (undetermined order), but implementors can override or otherwise implement this to ensure ordering of TaxProvider instances to be evaluated during discovery. This is useful when more than one tax provider may work for a given scenario, but where a one should be evaluated before others. The DefaultTaxDelegate optionally has a constructor arg that takes in a io.github.resilience4j.circuitbreaker.CircuitBreaker. If this is not null (default), then this CircuitBreaker will be used to increase resiliency a protect threads from being consumed if waiting for a response. You may override the bean definition for the TaxDelegate to supply a CircuitBreaker, which is not configured by default. Finally, the default discovery process is as follows:

    • Use the TaxCalculationRequest#getProviderId(). If not null and a TaxProvider with this ID exists, and the system can handle the request, then that TaxProvider is used. This allows the client to explicitly select the TaxProvider that is preferred directly in the request.

    • Evaluate TaxDelegateProperties#TaxDelegateProperties(ContextInfo). This attempts to find a preferred TaxProvider based on ID, starting with the application ID (if available), then the tenant ID (if available), then a default (if available). This allows various tenants or applications to configure a preferred TaxProvider. The properties associated with approach are: broadleaf.tax.delegate.application.{applicationID}.preferredTaxProviderId (application), broadleaf.tax.delegate.tenant.{tenantID}.preferredTaxProviderId (tenant), and broadleaf.tax.delegate.preferredTaxProviderId (default). This allows administrators to configure application or tenant-specific preferred Tax Providers, as well as a default preferred Tax Provider. All of these are optional.

    • Iterate over all remaining Tax Provider except the SimpleTaxProvider, if available, calling the canHandle method. This is where ordering (getOrder()) can be used to ensure certain providers are evaluated first.

    • If no Tax Provider was found, then evaluate the SimpleTaxProvider, if available, and call the canHandle method.

    • Finally, throw an IllegalStateException if no Provider was found that could handle the request.

    • Incidentally, we offer the concept of a "fallback provider". This is similarly configured via properties: broadleaf.tax.delegate.application.{applicationID}.fallbackTaxProviderId (application), broadleaf.tax.delegate.tenant.{tenantID}.fallbackTaxProviderId (tenant), and broadleaf.tax.delegate.fallbackTaxProviderId (default). If configured (and canHandle), this fallback provider will be executed if and only if the preferred provider results in an error, and the fallback provider is not the same as the preferred or discovered provider. Also, by default, the fallback provider is only used for Tax Calculation, which should never result in a customer being unable to price their cart or check out. It is not used for post-checkout operations. Post-checkout operations make use of Spring Cloud Stream to handle these events, and there is retry logic in these cases.

  • SimpleTaxProvider - Default, out-of-box implementation of TaxProvider that makes use of one or more SimpleTaxConfig definitions, defined in SimpleTaxProperties. These can be discriminated by application and/or tenant, and there can be a default configuration as described above. SimpleTaxProvider is a good candidate for the fallback provider in many cases because it uses in-memory logic and data to calculate (or estimate) taxes and this is often good enough for edge cases where there are problems calling a 3rd party provider.

  • SimpleTaxConfig - This is a JSON structure that can be deserialized into an object of a class with the same name, which defines tax rates for various jurisdictions, indicates if the tax is VAT, and indicates if tax exemptions are allowed. The SimpleTaxProvider makes use of SimpleTaxConfig for in-memory tax configuration and calculation. Because the SimpleTaxConfig file(s) are manually maintained, there is a risk of them being inaccurate. Therefore, SimpleTaxProvider is best used when no taxes should be applied, or as a fallback TaxProvider, in case an error occurs calling a more preferred tax provider.

Usage

As described, above, a TaxDelegate provides an "entry point" to delegate to a TaxProvider (potentially one of many). It encapsulates flexible discovery logic, checks that providers can handle the request, allows for optional CircuitBreaker invocation, and allows for optional fallback provider invocation.

Certain Broadleaf Microservices (e.g. Cart Operations Service and Order Operations Service) make use of the TaxCommon library. For example, see Cart Ops Tax Pricing Docs. In this case, there is an in-JVM service called DefaultDelegatingTaxService that implements TaxService. This replaces the deprecated DefaultTaxService which only made use of potentially 2 Tax Providers with no dispensation for different applications or tenants. During pricing, this TaxService is invoked. It constructs a TaxCalculationRequest and calls TaxDelegate#calculateTaxes(request, contextInfo). The TaxDelegate resolves the best TaxProvider to handle the request and invokes that provider. It then returns a TaxCalculationResponse, which contains the details of the tax calculation. It also contains the providerId of the TaxProvider that executed the request. The TaxService then maps the TaxCalculationResponse to tax details on the Cart. The TaxService also sets the tax provider ID of the tax provider that last calculated taxes for that cart, even if it was the fallback provider. This detail is saved on the Cart: cart.getInternalAttributes().put("calculationTaxProviderId", response.getTaxProviderId()). This will always provide a reference to the TaxProvider that last calculated taxes for that cart.

Order Operations Services similarly invokes commitTaxes, reverseTaxes and adjustTaxes. When taxes are committed, a similar attribute should be added to the Order. At this point, the Cart is immutable and we know who last calculated taxes. When taxes are committed, we want to save the property "commitTaxProviderId" with the name of the TaxProvider that committed the taxes. From this point on, any calls to reverseTaxes or adjustTaxes must be made with the same tax provider ID that executed the commit operation. This must be done by setting it on the ReverseTaxTransactionRequest or the AdjustTaxTransactionRequest.

Simple Tax Provider

SimpleTaxProvider is a default, out-of-box implementation of TaxProvider. This implementation allows for configurable tax tables in a JSON structure with varying layers of jurisdiction (Country, State/Province/Region, City, Postal Code).

The SimpleTaxProvider is acceptable for use in the following situations:

  • For calculating taxes in a demo or test context

  • For estimating taxes in any context, especially when there is a monetary or performance cost to using another TaxProvider for estimates

  • For calculating taxes in any context when this meets the needs of the implementor (e.g. when estimates are reasonable and appropriate, when no tax calculations are required at all and zero taxes will be returned, or when no tracking or remittance assistance is required of the Provider)

  • As a fallback TaxProvider, to be used when there is an error invoking another, more appropriate TaxProvider, usually to avoid friction in completing an order (it’s usually assumed that it’s better to allow a customer to complete an order with estimated taxes rather than introduce friction due to technical difficulties calling a preferred provider)

The Cart Operations Service defines a SimpleTaxProvider bean, by default. This means that it will be used for demo and default tax calculations if no other TaxProviders are introduced. By default, this component is automatically registered with a SimpleTaxConfig defined by classpath:simple-tax-example.json. Tenants and Applications can each define their own SimpleTaxConfig as a JSON Resource. The SimpleTaxConfig contains the following properties:

  • sampleConfig: Indicates that this is a sample or example. This is ignored by the JSON processor, and is generally set manually, as needed. (Defaults to false)

  • defaultRate: The default tax rate to be used if no other matching tax rate can be found

  • taxTables: A Map with a Country code as a key and a list of SimpleTaxRecords

SimpleTaxRecord declarations, defined in taxTables and defaultRate have the following properties:

  • countryDefault: Whether this record is the default for the mapped country

  • stateProvinceRegion: The state, province, or region to which this tax record applies

  • city: The city to which this tax record applies

  • postalCode: The postal code to which this tax record applies

  • rate: The tax rate for this record such as 0.0825, or 8.25%. (default is 0.0)

  • vat: Whether this record is a VAT tax (default is false)

  • allowTaxExemption: Whether this record supports or allows tax exemptions (default is true)

For example,

Sample Simple Configuration File
{
  "defaultRate": {"rate": "0.05"},
  "taxTables" : {
    "US": [{
      "countryDefault": true,
      "rate": "0"
    },
    {
      "stateProvinceRegion": "OK",
      "rate": "0.045"
    },
    {
      "stateProvinceRegion": "TX",
      "rate": "0.06375"
    },
    {
      "stateProvinceRegion": "TX",
      "city": "Celina",
      "rate": "0.0825"
    },
    {
      "stateProvinceRegion": "TX",
      "city": "Celina",
      "postalCode": "75009",
      "rate": "0.0625"
    },
    {
      "stateProvinceRegion": "TX",
      "city": "Plano",
      "rate": "0.0825"
    }],
    "CA": [{
      "countryDefault": true,
      "rate": "0.05"
    },
    {
      "stateProvinceRegion": "BC",
      "rate": "0.12"
    }],
    "UK": [{
      "countryDefault": true,
      "rate": "0.2",
      "vat": "true"
    }],
    "VAT5": [{
      "countryDefault": true,
      "rate": "0.05",
      "vat": "true"
    }]
  }
}

These properties provide an opportunity to manually maintain tax rates across multiple jurisdictions, including details about VAT and whether exemptions are allowed.

Additionally, you can define a default SimpleTaxConfig. This can be done via Properties resolved via SimpleTaxProperties:

  • Application: broadleaf.tax.provider.simple.application.{applicationID}.configPath=file:/path/to/applicaton/{applicationID}/tax-config.json

  • Tenant: broadleaf.tax.provider.simple.tenant.{tenantID}.configPath=file:/path/to/tenant/{tenantID}/tax-config.json

  • Default: broadleaf.tax.provider.simple.configPath=classpath:path/to/default-tax-config.json

The attempt to determine the appropriate SimpleTaxConfig for a given request and ContextInfo will be done in the following order:

  • Application - If ContextInfo is not null and contains an applicationId which maps to a Resource

  • Tenant - If no application-scoped config was found and ContextInfo is not null and contains an tenantId which maps to a Resource

  • Default - If no tenant-scoped config was found or ContextInfo is null and broadleaf.tax.provider.simple.configPath maps to a Resource

  • Example - If no SimpleTaxConfig could be found then the example (classpath:simple-tax-example.json) will be used and a warning will be logged each time taxes are calculated

For convenience, you may define the SimpleTaxConfig JSON directly using one of the following properties:

  • Application: broadleaf.tax.provider.simple.application.{applicationID}.configJsonString

  • Tenant: broadleaf.tax.provider.simple.tenant.{tenantID}.configJsonString

  • Default: broadleaf.tax.provider.simple.configJsonString

The configJsonString will be evaluated first, and if no value is found then configPath properties are evaluated. This can be useful if you have a very basic or simple config such as { }, which is a valid configuration that calculates zero taxes or {"defaultRate" : 0.05}, which would apply a rate of 5% across the board, regardless of jurisdiction. For more complicated SimpleTaxConfig details such as defining multiple jurisdictions it is recommended that you use configPath so that the details are defined in a file or a classpath resource.

In the United States sales tax calculation can be complicated due to the fact that there are so many independent jurisdictions. While it is not normally advised to use the SimpleTaxProvider as a primary tax provider, except in situations defined above, it may be useful to use it as a fallback provider. In this cases, you’ll want to define an average tax rate for each of the main jurisdictions to which you are selling. Different jurisdictions may contribute to the tax rate in an additive way (e.g. State/Province/Region’s tax rate may be 6.25%, but there may be county and city taxes that boost the overall tax rate to 8.25% for a given address). For the United States, applying an average tax rate for each State or Territory can be helpful for fallback purposes (e.g. Texas has an average tax rate of 8.25% or 0.0825). Rather than trying to provide granular tax rates for each city and/or county, using an average tax rate for the State can be helpful). The same thing may be true for Countries that have a national sales tax. However, configuring average tax rates for the Countries and Regions to which you sell can allow you to collect taxes approximate for sales in a situation where a more appropriate tax provider fails and the SimpleTaxProvider is configured as the fallback.

In order to configure the SimpleTaxProvider as the default fallback, you can set the following property:

  • broadleaf.tax.delegate.fallbackTaxProviderId=BLC_SIMPLE_TAX_PROVIDER

In order to configure the SimpleTaxProvider as the fallback for a particular Application or Tenant, you can set one or both of the following properties:

  • broadleaf.tax.delegate.application.{applicationID}.fallbackTaxProviderId=BLC_SIMPLE_TAX_PROVIDER, or

  • broadleaf.tax.delegate.tenant.{tenantID}.fallbackTaxProviderId=BLC_SIMPLE_TAX_PROVIDER

The the fallback provider will be quietly ignored if it is the same as the same as the primary or preferred provider selected by the . The fallback provider is only used for tax calculation. It is not used for commit, reverse, or adjust operations. These operations typically occur on the receipt of event messages , which means they happen in a background thread and do not directly affect the customer experience. They also benefit from retry logic associated with message consumption.

Caution
If intended to be used in production for calculating taxes, be aware that the implementor or user of this component is responsible for accurate configuration of SimpleTaxConfig and SimpleTaxRecord instances, and for collection, reporting, and payment of relevant taxes to various tax authorities and jurisdictions. Implementors or users of this component must maintain the tax configuration details and must ensure that taxes are paid according to what was collected and what is legally owed, which may vary depending on the accuracy of the configuration.

Tax Exemption

Tax exemption capabilities depend on the TaxProvider implementation being used. The TaxCalculationRequest has a field called taxExemptionCode. This can be used to pass a tax exemption code or identifier to the Provider as a hint or data point to allow that Provider to avoid calculating taxes or to provide tax exemptions. The SimpleTaxProvider handles exemptions by default, with a field on SimpleTaxRecord called allowTaxExemption. This defaults to true. If a SimpleTaxRecord is selected that supports exemptions, then the SimpleTaxProvider will calculate zero taxes if TaxCalculationRequest#getTaxExemptionCode() returns a non-blank value. Note that the SimpleTaxProvider does not in any way validate the taxExemptionCode. This is a trusted field, and if it is not blank or null then the taxExemptionCode will be trusted and zero taxes will be applied.

Please refer to the documentation for other TaxProvider integrations to determine their tax exemption capabilities and requirements.

VAT Taxes

VAT stands for Value Added Tax.

See VAT Taxes article for information and configuration.

Creating a new TaxProvider

Broadleaf Commerce may provide multiple TaxProvider implementations (e.g. SimpleTaxProvider, AvalaraTaxProvider, VertexTaxProvider, etc.). The TaxDelegate, if used by the client, will be responsible for selecting the most appropriate TaxProvider using a flexible discovery process as described above. The TaxDelegate is also responsible for invoking the selected TaxProvider. The TaxDelegate will also handle fallback logic, if configured, which can be useful if errors occur using the preferred provider or if a CircuitBreaker is open.

Broadleaf clients and 3rd party SIs may prefer to implement a TaxProvider. Implementing a TaxProvider can be relatively straight forward. Here are some guidelines:

  • Create a custom implementation of TaxProvider (e.g. public class MyTaxProvider <T1 extends TaxRequest, T2 extends TaxResponse>implements TaxProvider<T1, T2> - Note that the generics here are only for backwards compatibility. TaxRequest and TaxResponse are deprecated.)

  • Do not extend SimpleTaxProvider unless you are simply overriding the behavior of that component (and therefore overriding the Bean definition). Do not use it as a starting point for creating net new TaxProvider implementations. The reason is that the getProviderId() method is final in SimpleTaxProvider. Each TaxProvider instance must provide a different value returned from getProviderId(). Otherwise, the default construction of the DefaultTaxDelegate may throw an exception if it encounters duplicate provider IDs.

  • Implement the methods described above:

    • public String getProviderId() - The returned value should be a non-null, non-empty String with no spaces, and without a "BLC_" prefix (e.g. "MY_TAX_PROVIDER"). All TaxProvider implementations provided by Broadleaf Commerce should have a provider ID that starts with "BLC_".

    • public <R extends TaxCalculationRequest> boolean canHandle(@NonNull R request, @Nullable ContextInfo contextInfo)

    • public List<T2> calculateTaxes(List<T1> taxRequests, @Nullable ContextInfo contextInfo) - This method is arguably the most important, along with canHandle because it’s used to calculate taxes for the purpose of pricing.

    • public CommitTaxResponse commitTaxes(CommitTaxRequest request, @Nullable ContextInfo contextInfo) - This should be a pass-through if nothing can be done here to avoid an UnsupportedOperationException, which is the default.

    • public TaxCalculationResponse adjustTaxes(AdjustTaxTransactionRequest request, @Nullable ContextInfo contextInfo) - This should be a pass-through if nothing can be done here to avoid an UnsupportedOperationException, which is the default. Generally, if commitTaxes is a no-op method, then this should be as well, since this is meant to modify what was previously committed.

    • public ReverseTaxTransactionResponse reverseTaxTransaction(ReverseTaxTransactionRequest request, @Nullable ContextInfo contextInfo) - This should be a pass-through if nothing can be done here to avoid an UnsupportedOperationException, which is the default. Generally, if commitTaxes is a no-op method, then this should be as well, since this is meant to reverse what was previously committed.

  • Create a Spring Bean definition:

The following example simply demonstrates the bean definition for a custom TaxProvider, presumably configured in service/cartops/src/main/java/com/broadleafdemo/cartops/DemoCartOperationServiceAutoConfiguration, or equivalent, for example:

// Note that bean name is important as there may be multiple TaxProvider implementations
@Bean(name = "myCustomTaxProvider")
public TaxProvider<TaxRequest,TaxResponse> myCustomTaxProvider(...) {
    return new MyCustomTaxProvider(...);
}

The above example also demonstrates how to override an existing TaxProvider. For example, if you have extended or overridden the Avalara Tax Provider, you would have extended AvalaraTaxProvider and used the same bean name:

@Bean(name = "avalaraTaxProvider")
public TaxProvider<TaxRequest,TaxResponse> avalaraTaxProvider(...) {
    return new MyAvalaraTaxProvider(...);
}

If you are building a TaxProvider implementation that will be packaged and distributed (similar to the way we build 3rd party TaxProviders that are maintained by Broadleaf Commerce), then you will likely need to implement the TaxProvider interface, provide a Properties mapping, and create a default AutoConfiguration:

public class AnotherTaxProvider<T1 extends TaxRequest, T2 extends TaxResponse> implements TaxProvider<T1, T2> {

    public AnotherTaxProvider(TypeFactory typeFactory, AnotherTaxProviderProperties properties, ...) {
        ...
    }

    @Override
    public final String getTaxProviderId() {
        return "MY_COMPANY_ANOTHER_TAX_PROVIDER";
    }

    @Override
    public <R extends TaxCalculationRequest> boolean canHandle(@NonNull R request,
            @Nullable ContextInfo contextInfo) {
        // This is naive but makes the point...
        // Typically, this would look for credentials or something that can be used to determine if this tax provider can
        // be invoked by for this request and with this ContextInfo.
        return getProperties().getAnotherProperty(contextInfo) != null;
    }

    // Additional methods such as calculateTaxes, commitTaxes, reverseTaxes, adjustTaxes, and optionally getOrder
    // Avoid implementing methods that are deprecated in the interface as these should generally not be used and have default behavior.
    ...
}

@Setter
@Getter
@ConfigurationProperties("broadleaf.tax.provider.another")
public class AnotherTaxProviderProperties extends DiscriminatedProperties<AnotherTaxProviderProperties> {

    private String anotherProperty;

    public String getAnotherProperty(ContextInfo contextInfo) {
        // This goes through a process to try to resolve the property for
        // application, then tenant, then default, in that order, based on ContextInfo, if not null.
        return getField("anotherProperty", contextInfo);
    }
}

@Configuration
@EnableConfigurationProperties(value = {AnotherTaxProviderProperties.class})
public class AnotherTaxProviderAutoConfiguration {

    @Bean(name = "anotherTaxProvider")
    @ConditionalOnMissingBean(name = "anotherTaxProvider")
    public TaxProvider<TaxRequest, TaxResponse> anotherTaxProvider(
            AnotherTaxProviderProperties properties,
            TypeFactory typeFactory,
            ...) {
        return new AnotherTaxProvider<>(typeFactory, properties, ...);
    }
}

In src/main/resources/META-INF/spring.factories, we reference the AnotherTaxProviderAutoConfiguration, above:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.anothercompany...autoconfigure.AnotherTaxProviderAutoConfiguration

Ideally, a single TaxProvider should handle all Tax operations for the lifecycle of a Cart and its associated submitted Order. However, the same TaxProvider must handle all tax operations for an Order once taxes are committed. As a result, implementations must consider how to handle adjustTaxes and reverseTaxes after commitTaxes has been called. Typically, commitTaxes notifies a tax reporting system of taxes collected and therefore owed. The other methods are meant to adjust those details after a commit is executed.