Questions regarding DefaultTaxCalculator implementation

Questions regarding DefaultTaxCalculator implementation

 

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

    Thanks Mark, Bein and Quan for all your inputs.

    Much appreciated!

    #195306 Jul 19, 2018 15:29
  •  

    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;
    
            /// 
            ///     Default contructor
            /// 
            /// 
            /// 
            public AvaOrderFormCalculator(IShippingCalculator shippingCalculator, AvataxManager avataxManager) :
                base(shippingCalculator)
            {
                _avataxManager = avataxManager;
            }
    
            /// 
            ///     Calculates the tax in one request
            /// 
            /// 
            /// 
            /// 
            /// 
            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;
    
            /// 
            ///     Constructor for the ava tax calculator
            /// 
            /// 
            public AvaTaxCalculator(AvataxManager avataxManager)
            {
                _avataxManager = avataxManager;
            }
    
            /// 
            ///     Gets the shipping tax of the line item
            /// 
            /// 
            /// 
            /// 
            /// 
            /// 
            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);
            }
    
            /// 
            ///     Calculates the sales tax of the lineitem
            /// 
            /// 
            /// 
            /// 
            /// 
            /// 
            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);
            }
    
            /// 
            ///     Gets the tax total for the reurn order form.
            /// 
            /// The return order form.
            /// The market.
            /// The currency.
            /// 
            public Money GetReturnTaxTotal(IReturnOrderForm returnOrderForm, IMarket market, Currency currency)
            {
                return new Money(0, currency);
            }
    
            /// 
            ///     Gets the tax total for the return shipment.
            /// 
            /// The shipment.
            /// The market.
            /// The currency.
            /// 
            public Money GetShippingReturnTaxTotal(IShipment shipment, IMarket market, Currency currency)
            {
                return new Money(0, currency);
            }
    
            /// 
            ///     Gets the tax total for the shipment.
            /// 
            /// The shipment.
            /// The market.
            /// The currency.
            /// 
            public Money GetShippingTaxTotal(IShipment shipment, IMarket market, Currency currency)
            {
                return new Money(0, currency);
            }
    
            /// 
            ///     Gets the tax total for the order group
            /// 
            /// The order group.
            /// The market.
            /// The currency.
            /// 
            public Money GetTaxTotal(IOrderGroup orderGroup, IMarket market, Currency currency)
            {
                return new Money(0, currency);
            }
    
            /// 
            ///     Gets the tax total for the order form.
            /// 
            /// The order form.
            /// The market.
            /// The currency.
            /// 
            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;
                
            }
        }

    @Mark Hall: Thank you for the inputs, this really helped a lot. I overwrote the OrdermFormCalculator and now I see the taxes flowing through. Also, the new EPI tax fields are populated per line item with their corresponding taxes and also shipment tax is stored as well. Until here I am good, but I am not able to understand where and how the ITaxcalculator implementation helps. How does this solve the problem of the back and forth with the 3rd party service. Cause when I debug the code I still see that CalculateTaxTotal is still be called multiple times. I am also attaching the code for your reference.

    Class to overwrite the DefaultOrderFormCalculator

    public class CustomOrderFormCalculator : DefaultOrderFormCalculator
    { 
    public CustomOrderFormCalculator(IShippingCalculator shippingCalculator)
    : base(shippingCalculator)
    {
    
    }
    /// <summary>
    /// Overide the tax total computation with custom logic or have the default implementation if none exists
    /// </summary>
    /// <param name="orderForm"></param>
    /// <param name="market"></param>
    /// <param name="currency"></param>
    /// <returns>Tax Total</returns>
    protected override Money CalculateTaxTotal(IOrderForm orderForm, IMarket market, Currency currency)
    {
    var taxImplementationObj = ServiceLocator.Current.GetInstance<ICustomTaxCalculator>();
    Money totalTax = new Money(0.0M, currency);
    if (orderForm.Shipments.Count > 0)
    {
    if (taxImplementationObj != null)
    {
    
    totalTax = taxImplementationObj.GetSalesTax(orderForm, market, currency);
    }
    else
    {
    totalTax = base.CalculateTaxTotal(orderForm, market, currency);
    }
    }
    return totalTax;
    }
    
    }

    Class that Implements ITaxCalculator:

    public class TaxCalculator : ITaxCalculator
    {
    /// <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);
    }
    }
    
    
    
    



    #195367 Edited, Jul 20, 2018 21:04
First   1 2   Last