Broadleaf Microservices
  • v1.0.0-latest-prod

TaxService

Overview

TaxService is an internal service responsible for providing and applying tax calculations for a cart. This is not a stand-alone microservice and cannot be accessed directly via a REST API. Rather, this service is internal to Cart Operations Service and is invoked as part of the Pricing functionality.

Note
For overall pricing, see CartPricingService.

Default Implementation Details

The default implementation of this interface is DefaultDelegatingTaxService, which replaces the deprecated DefaultTaxService. The default implementation defers to TaxDelegate, a component in the TaxCommon library, which selects the best TaxProvider and delegates to that.

The default implementation defers the actual calculating to the configured TaxProvider(s).

Tip
The TaxDelegate can select from a list of TaxProvider instances and uses a discovery process to find the best implementation for the provided request and ContextInfo. This allows different tenants and applications to make use of different TaxProviders.

Requirements for Actual Taxes Calculation

Out of the box, having an address is required to calculate actual taxes, whether it’s shipping or billing address, depending on the fulfillment’s taxAddressSource. Note: If requesting estimated tax calculation, the address may not be needed, depending on whether any of the registered tax providers can declare a reasonable default rate.

For different requirements, you can override DefaultDelegatingTaxService#canCalculateActualTaxes to customize its behavior.

SimpleTaxProvider

The SimpleTaxProvider is a basic implementation of a TaxProvider that is primarily suitable for estimating taxes, but may be effectively used as a fallback provider in case a more appropriate provider is unavailable. It reads in tax details from a file or from a configuration property. For more information, see Tax Common

Configuring Included Taxes

SimpleTaxProvider supports both included & additional taxes. By default, taxes are added on top of the item’s price. To enable included prices, you’ll need to customize DefaultDelegatingTaxService#isItemIncludesTaxes(Cart, FulfillmentGroup, FulfillmentItem, CartItem, ContextInfo). The result of this method gets passed to the SimpleTaxProvider, which is used to determine how taxes will be calculated.

See VAT Taxes article for information and configuration.

Configuring Tax Codes

Tax Codes can be configured on the product or category. The SimpleTaxProvider will use an item’s tax code (if present) to look up tax record configuration. If no configuration matches the tax code, then the SimpleTaxProvider will look up configuration based on the country code of the shipping address.

Reporting Tax and Taxable Amounts

Each FulfillmentItem contains a list of TaxDetails, which in turn contain fields for the amount of tax calculated and the taxable amount of the items. The SimpleTaxProvider and DefaultTaxService will set these fields consistent with the tax type (sales tax or VAT).

The CartPricing object on Cart has additional fields to indicate if the cart’s taxes are included (VAT) or excluded (sales tax) from the subtotal.

  • The field "taxIncludedType" maps to the TaxIncludedType enum with possible values of YES, NO, and PARTIAL.

  • The field "includedTaxAmount" indicates the amount of tax that is included in the cart subtotal.

For CartPricing objects with only VAT taxes, the "taxIncludedType" will be "YES" and the "includedTaxAmount" will equal the "totalTax" field. When displaying taxes and totals to a user, avoid doing any calculations on the front-end and use the appropriate CartPricing fields.

Other Tax Providers

Broadleaf, as well as 1st and 3rd parties, may provide additional TaxProvider implementations. Each one should have a unique bean name. Each registered TaxProvider will be passed to the DefaultTaxDelegate upon startup.

In order to use these additional TaxProvider implementation, one should only need to include them in the classpath. For example:

<dependency>
    <groupId>com.broadleafcommerce.microservices</groupId>
    <artifactId>broadleaf-avalara-tax</artifactId>
    <version>1.0.0-GA</version>
</dependency>

This will register the AvalaraTaxProvider at runtime, which will add it to a list provided to DefaultTaxDelegate. The process to resolve the best TaxProvider is defined here: Tax Common.

Common Use Cases & How to Achieve Them

Out of the box, SimpleTaxProvider is used to calculate actual taxes. In your environment, you may have different requirements and constraints. This section will go through the following common use cases as well as how to achieve them:

Note
Cart#isTaxEstimated is used to indicate whether the tax calculation is estimated

Use Estimated Taxes, Until an Address is Provided By the Customer

In your project, you may have a geolocation service that can guess the address of the cart before the customer can provide the actual address. At this point, you may want to indicate that the tax calculation is estimated.

To achieve this, you can add an attribute to Cart’s attributes map to when the address is an estimated address, then override DefaultCartPricingService#determineTaxCalculationStrategy to something like the following:

public class MyPricingService extends DefaultCartPricingService {

    public MyPricingService(CartItemPricingUtils cartItemPricingUtils, CartProvider cartProvider,
            PricingProvider pricingProvider, OfferProvider offerProvider,
            CatalogProvider<? extends CatalogItem> catalogProvider,
            CartItemConfigurationService<? extends CatalogItem> cartItemConfigurationService,
            TaxService taxService, FulfillmentPricingService fulfillmentPricingService,
            ObjectMapper objectMapper, CartTotalsCalculator cartTotalsCalculator,
            CartPricingRoundingHelper roundingHelper,
            CartOperationServiceProperties cartOperationServiceProperties, TypeFactory typeFactory,
            MessageSource messageSource) {
        super(cartItemPricingUtils, cartProvider, pricingProvider, offerProvider, catalogProvider,
                cartItemConfigurationService, taxService, fulfillmentPricingService, objectMapper,
                cartTotalsCalculator, roundingHelper, cartOperationServiceProperties, typeFactory,
                messageSource);
    }

    @Override
    protected String determineTaxCalculationStrategy(@lombok.NonNull Cart cart,
                                                     @Nullable ContextInfo contextInfo) {
        if (getTaxService().canCalculateTaxes(cart, contextInfo)) {
            boolean isGeoLocationAddress = cart.getFulfillmentGroups().stream()
                    .anyMatch(fg ->
                            BooleanUtils.isTrue((Boolean) fg.getAttributes().get("IS_GEO_ADDRESS")));
            if (isGeoLocationAddress) {
                return DefaultTaxCalculationStrategies.ESTIMATED.name();
            } else {
                return DefaultTaxCalculationStrategies.ACTUAL.name();
            }
        } else {
            return DefaultTaxCalculationStrategies.SKIP.name();
        }
    }
}

Then, override DefaultDelegatingTaxService#determinePreferredProviderId to use a dedicated tax provider for estimated tax calculation as such:

public class MyDelegatingTaxService extends DefaultDelegatingTaxService {
    public MyDelegatingTaxService(@lombok.NonNull TaxDelegate<?, ?> taxDelegate,
            List<TaxAddressSourceHandler> taxAddressSourceHandlers,
            @lombok.NonNull TypeFactory typeFactory) {
        super(taxDelegate, taxAddressSourceHandlers, typeFactory);
    }

    @Override
    protected String determinePreferredProviderId(@lombok.NonNull Cart cart,
            boolean estimate,
            @Nullable ContextInfo contextInfo) {
        if (estimate) {
            return "estimatedTaxProviderId";
        } else {
            return "actualTaxProviderId";
        }
    }
}

This way, after tax calculation, you can use the Cart#isTaxEstimated flag to indicate that the tax calculation was estimated.

Important
DefaultDelegatingTaxService#canCalculateTaxes is determined based on the sufficiency of the fulfillment groups' addresses. At the bare minimum, the address must have stateProvinceRegion and country for taxes to be calculated. For custom behavior, please override DefaultDelegatingTaxService#fulfillmentGroupHasSufficientAddress

Use Estimated If the Actual Tax Calculation Fails

To achieve this, you simply need to configure the preferred and fall back tax providers via TaxDelegateProperties (discriminated by tenant & application):

  • broadleaf.tax.delegate.preferred-tax-provider-id

  • broadleaf.tax.delegate.fallback-tax-provider-id

With this configuration, if the primary tax provider fails, the fallback tax provider would be used, which in this case would be an estimated tax provider.

Additionally, you can also override DefaultDelegatingTaxService#determinePreferredProviderId to identify the preferred tax provider based on Cart and/or ContextInfo information, rather than using the property.