Questions regarding DefaultTaxCalculator implementation

Siddharth Gupta
Member since: 2018
 

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
  • Quan Mai
    Member since: 2011
     

    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
  • Islam Hamed
    Member since: 2014
     

    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
  • Quan Mai
    Member since: 2011
     

    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
  • Islam Hamed
    Member since: 2014
     

    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
  • Quan Mai
    Member since: 2011
     

    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
  • Mark Hall
    Member since: 2011
     

    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
  •  

    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
  • Quan Mai
    Member since: 2011
     

    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
  • Islam Hamed
    Member since: 2014
     

    Thanks Mark, Bein and Quan for all your inputs.

    Much appreciated!

    #195306 Jul 19, 2018 15:29
  • Siddharth Gupta
    Member since: 2018
     
    #195367 Edited, Jul 20, 2018 21:04
  • Quan Mai
    Member since: 2011
     

    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
  • Siddharth Gupta
    Member since: 2018
     

    @Quan: Thank you!

    #195369 Jul 20, 2018 21:27
  • Mark Hall
    Member since: 2011
     

    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