Questions regarding DefaultTaxCalculator implementation

Vote:
 

Hi, 

I plan to override the CalculateSalesTax method of DefaultTaxCalculator with our custom tax calculation via Avalara. On seeing the method signature I see that it is ILineItem and not a collection of ILineItems. It means that we will calculate tax per line item and that makes me wary of some potential issues:

1) Performace, by calculating the tax per line item; all custom implementations will have an overhead of doing back and forth for each line item.

2) Cost, the 3rd party tax calculators charge per call. So now the charges will shoot up drastically if we were to calculate tax per line item.

How can we achieve a good solution in this scenario?

Below is the method signature for reference:

 CalculateSalesTax(ILineItem lineItem, IMarket market, IOrderAddress shippingAddress, Money basePrice)



Regards,

Siddharth

#195168
Jul 17, 2018 0:30
Vote:
 

Interesting. In Commerce 12 we improved tax calculating performance by adding cache, but I guess we can still improve further. We will look into this. Thanks

#195170
Jul 17, 2018 6:38
Vote:
 

Hey @Quan Mai,

Does this mean there is no method we can override that takes ILineItem(s) collection, Shipping address and return the final taxes amount?

I hope you will say there is a method for that please :)

Thanks!

#195206
Jul 17, 2018 17:23
Vote:
 

No there is none now. The problem is adding a new method to an interface is a breaking change. That's why we can do it any sooner than Commerce 13

#195209
Jul 17, 2018 19:51
Vote:
 

Thanks Quan for your input!

For the last 2 years, we have been talking with EPiServer technical team about Taxes calculations performance and Cost because of the multiple calls to Avalara.

The promise was that EPi 12 will have all issues resolved. It is disappointing to hear that we need to wait for Commerce 13 to get this sorted out.

Meanwhile, do you have any suggestions on how to achieve what we need in one single call to Avalara that will work with Order Processing module?

#195213
Edited, Jul 17, 2018 20:26
Vote:
 

Commerce 12 did improve tax system quite a lot, however we didn't receive requests for batch processing. Commerce 13 might happen faster than you think!

#195214
Jul 17, 2018 22:06
Vote:
 

You can easily make this work and I dont think it should be changed to a collection because tax is handled at each individual line item level now.  By overwriting the OrderFormCalculator you can send your request for the order form.  Then in the tax calculator you just use the already calculated values.

public class AvaOrderFormCalculator : DefaultOrderFormCalculator
    {
        private readonly AvataxManager _avataxManager;

        /// <summary>
        ///     Default contructor
        /// </summary>
        /// <param name="shippingCalculator"></param>
        /// <param name="avataxManager"></param>
        public AvaOrderFormCalculator(IShippingCalculator shippingCalculator, AvataxManager avataxManager) :
            base(shippingCalculator)
        {
            _avataxManager = avataxManager;
        }

        /// <summary>
        ///     Calculates the tax in one request
        /// </summary>
        /// <param name="orderForm"></param>
        /// <param name="market"></param>
        /// <param name="currency"></param>
        /// <returns></returns>
        protected override Money CalculateTaxTotal(IOrderForm orderForm, IMarket market, Currency currency)
        {
            var result = _avataxManager.CalculateTaxAsync(orderGroup, orderForm, false, orderGroup is IPurchaseOrder)
                .GetAwaiter()
                .GetResult();

            if (result?.Response == null) return new Money(0, currency);
            _avataxManager.ApplyCalculatedTaxes(orderForm, result);
            return new Money(result.Response.totalTax ?? 0m, currency);
        }
    }

    public class AvaTaxCalculator : ITaxCalculator
    {
        private readonly AvataxManager _avataxManager;

        /// <summary>
        ///     Constructor for the ava tax calculator
        /// </summary>
        /// <param name="avataxManager"></param>
        public AvaTaxCalculator(AvataxManager avataxManager)
        {
            _avataxManager = avataxManager;
        }

        /// <summary>
        ///     Gets the shipping tax of the line item
        /// </summary>
        /// <param name="lineItem"></param>
        /// <param name="market"></param>
        /// <param name="shippingAddress"></param>
        /// <param name="basePrice"></param>
        /// <returns></returns>
        public Money GetShippingTax(ILineItem lineItem, IMarket market, IOrderAddress shippingAddress, Money basePrice)
        {
            Validator.ValidateArgNotNull(nameof(lineItem), lineItem);
            Validator.ValidateArgNotNull(nameof(market), market);
            Validator.ValidateArgNotNull(nameof(shippingAddress), shippingAddress);
            Validator.ValidateArgNotNull(nameof(basePrice), basePrice);

            if (lineItem is ILineItemCalculatedAmount lineItemCalculatedAmount)
                return new Money(lineItemCalculatedAmount.SalesTax, basePrice.Currency);

            return new Money(0, basePrice.Currency);
        }

        /// <summary>
        ///     Calculates the sales tax of the lineitem
        /// </summary>
        /// <param name="lineItem"></param>
        /// <param name="market"></param>
        /// <param name="shippingAddress"></param>
        /// <param name="basePrice"></param>
        /// <returns></returns>
        public Money GetSalesTax(ILineItem lineItem, IMarket market, IOrderAddress shippingAddress, Money basePrice)
        {
            Validator.ValidateArgNotNull(nameof(lineItem), lineItem);
            Validator.ValidateArgNotNull(nameof(market), market);
            Validator.ValidateArgNotNull(nameof(shippingAddress), shippingAddress);
            Validator.ValidateArgNotNull(nameof(basePrice), basePrice);

            if (lineItem is ILineItemCalculatedAmount lineItemCalculatedAmount)
                return new Money(lineItemCalculatedAmount.SalesTax, basePrice.Currency);

            return new Money(0, basePrice.Currency);
        }

        /// <summary>
        ///     Gets the tax total for the reurn order form.
        /// </summary>
        /// <param name="returnOrderForm">The return order form.</param>
        /// <param name="market">The market.</param>
        /// <param name="currency">The currency.</param>
        /// <returns></returns>
        public Money GetReturnTaxTotal(IReturnOrderForm returnOrderForm, IMarket market, Currency currency)
        {
            return new Money(0, currency);
        }

        /// <summary>
        ///     Gets the tax total for the return shipment.
        /// </summary>
        /// <param name="shipment">The shipment.</param>
        /// <param name="market">The market.</param>
        /// <param name="currency">The currency.</param>
        /// <returns></returns>
        public Money GetShippingReturnTaxTotal(IShipment shipment, IMarket market, Currency currency)
        {
            return new Money(0, currency);
        }

        /// <summary>
        ///     Gets the tax total for the shipment.
        /// </summary>
        /// <param name="shipment">The shipment.</param>
        /// <param name="market">The market.</param>
        /// <param name="currency">The currency.</param>
        /// <returns></returns>
        public Money GetShippingTaxTotal(IShipment shipment, IMarket market, Currency currency)
        {
            return new Money(0, currency);
        }

        /// <summary>
        ///     Gets the tax total for the order group
        /// </summary>
        /// <param name="orderGroup">The order group.</param>
        /// <param name="market">The market.</param>
        /// <param name="currency">The currency.</param>
        /// <returns></returns>
        public Money GetTaxTotal(IOrderGroup orderGroup, IMarket market, Currency currency)
        {
            return new Money(0, currency);
        }

        /// <summary>
        ///     Gets the tax total for the order form.
        /// </summary>
        /// <param name="orderForm">The order form.</param>
        /// <param name="market">The market.</param>
        /// <param name="currency">The currency.</param>
        /// <returns></returns>
        public Money GetTaxTotal(IOrderForm orderForm, IMarket market, Currency currency)
        {
            return new Money(0, currency);
        }
    }

    public class AvataxManager
    {
        public virtual void ApplyCalculatedTaxes(IOrderForm orderForm)
        {
            var configuration = GetConfiguration();
            var response = result.Response;
            
            // clear all shipping taxes.
            foreach (var shipment in orderForm.Shipments)
            {
                foreach (var lineItem in shipment.LineItems)
                    if (lineItem is ILineItemCalculatedAmount lineItemCalculatedAmount)
                        lineItemCalculatedAmount.SalesTax = 0m;
                shipment.Properties["ShippingTax"] = 0m;
            }

            // set shipping taxes.
            if (response.lines != null)
                foreach (var line in response.lines)
                {
                    var shipment = _taxDocumentFactory.GetShipmentByLineNumber(orderForm, line.lineNumber);
                    if (shipment != null)
                    {
                        shipment.Properties["ShippingTax"] = line.tax;
                        continue;
                    }

                    var lineItem = _taxDocumentFactory.GetLineItemByLineNumber(orderForm, line.lineNumber);
                    if (lineItem is ILineItemCalculatedAmount lineItemCalculatedAmount)
                    {
                        lineItemCalculatedAmount.SalesTax = line.tax ?? 0m;
                        lineItemCalculatedAmount.IsSalesTaxUpToDate = true;
                    }
                }

            //// set order totals (shipping taxes are already included in response.TotalTax).
            orderForm.Properties["TaxTotal"] = response.totalTax;
            
        }
    }
#195252
Jul 18, 2018 16:57
Vote:
 

Hi!

As Mark Hall said, the tax rate is configured on line item level. A shipment can have multiple line items and each line item can have different tax rate. I.e line items in a shipment have the same shipping address definitively but might have different tax rates, that why in the tax calculator, it takes a single line item as parameter. 

Regarding to the cost of calling 3rd tax calculators, as Quan said, in Commerce 12 we cached the tax amounts (also the tax rates of line items), so that the tax calculator won't be called multiple times unless there're changes affecting to the tax such as line item quantity, shipping address,...

/B.

#195274
Jul 19, 2018 8:44
Vote:
 

I'm less familiar with Avatax but it's possible that their APIs can take a batch of lineitems with prices and configured tax rate. It does not hurt to have an API to get tax rates of multiple lineitems, by default implementation we can calculate one by one, but customers who want to can do batch processing. Again that's a breaking change 

#195275
Jul 19, 2018 8:54
Vote:
 

Thanks Mark, Bein and Quan for all your inputs.

Much appreciated!

#195306
Jul 19, 2018 15:29
Vote:
 

As Mark's post solved your problem, I marked it as accepted. You are welcome to do so yourself :) 

We will look into the problem and see if can improve it further in Commerce 13

#195368
Jul 20, 2018 21:21
Vote:
 

@Quan: Thank you!

#195369
Jul 20, 2018 21:27
Vote:
 

I serialize the the request and response on OrderForm and check the the request is the same than the one that will happen.  If they match then I return the response from the OrderForm so no other request is made to the service.

#195370
Jul 20, 2018 21:35
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.