Avalara address validation integration into Optimizely B2B Commerce Cloud
written by Shahira Bhanu Sheik
|October 2021
Calculating tax during checkout is one of the most important aspects of e-commerce checkout functionality. Optimizely B2B Commerce Cloud provides integration with Avalara out of the box, but the tax calculator plug-in doesn’t validate addresses when calculating tax using their API. The correctness of tax calculation depends on a valid address.
In this blog post, I will discuss how I made use of the Avalara address validation API to alert the user that entered address is not valid before proceeding to calculate tax.
The issue
We came to know about the issue when one of Nishtech’s clients alerted us that there are differences in calculated tax in the ERP compared to the orders created on the e-commerce storefront. Although the differences are small, it was creating problems with customer invoicing.
This issue was happening for orders placed in B2C sites and for the dropship orders in B2B sites. When we looked at those orders, we found that there is no exact match of the shipping addresses of those orders in the Avalara address database.
I started looking into Avalara getTax call to understand how the address is used to calculate tax. Here are the steps it goes through before responding:
- Address validation with the street address in the Avalara database
- If the address does not validate, it then validates the zip+4 code passed with the address
- If the above fails, it then validates the city and state or 5-digit zip code on the address
Any one of the above conditions is validated then it returns the tax amount. But the calculated tax may not be correct because of the incomplete address.
The solution
To resolve this issue, we customized the out of the box Avalara tax plug-in to include stricter address validation utilizing the Avalara address validation API before calculating the tax. Below is a sample request and corresponding response from the address validation API.
Request:
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Header> <Profile xmlns="http://avatax.avalara.com/services"> <Name>5.7.3.1</Name> <Client>InSite eCommerce</Client> <Adapter>Avalara.AvaTax.Adapter,5.7.3.1</Adapter> <Machine>machineName</Machine> </Profile> <Security soap:mustUnderstand="1" soap:actor="http://schemas.xmlsoap.org/soap/actor/next" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> <UsernameToken> <Username>username</Username> <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">password</Password> </UsernameToken> </Security> </soap:Header> <soap:Body> <Validate xmlns="http://avatax.avalara.com/services"> <ValidateRequest> <Address> <Line1>100 e business way</Line1> <Line2>suite 101</Line2> <City>delaware</City> <Region>CA</Region> <PostalCode>45291</PostalCode> <Country>US</Country> <TaxRegionId>0</TaxRegionId> </Address> <TextCase>Default</TextCase> <Coordinates>false</Coordinates> <Taxability>false</Taxability> </ValidateRequest> </Validate> </soap:Body> </soap:Envelope>
Response:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <ValidateResponse xmlns="http://avatax.avalara.com/services"> <ValidateResult> <TransactionId>6544627245943902</TransactionId> <ResultCode>Error</ResultCode> <Messages> <Message Name="CityError"> <Summary>The city could not be determined.</Summary> <Details>The city could not be found or determined from postal code.</Details> <HelpLink>http://www.avalara.com</HelpLink> <RefersTo>Address.City</RefersTo> <Severity>Error</Severity> <Source>Avalara.AvaTax.Common</Source> </Message> </Messages> <ValidAddresses> <ValidAddress> <AddressCode/> <Line1>100 e business way</Line1> <Line2>suite 101</Line2> <Line3/> <City>delaware</City> <Region>CA</Region> <PostalCode>45291</PostalCode> <Country>US</Country> <TaxRegionId>0</TaxRegionId> <Latitude/> <Longitude/> <Line4>delaware CA 45291</Line4> <County/> <FipsCode/> <CarrierRoute/> <PostNet/> <AddressType>N</AddressType> <ValidateStatus>AInvalidCity</ValidateStatus> <GeocodeType/> </ValidAddress> </ValidAddresses> <Taxable>false</Taxable> </ValidateResult> </ValidateResponse> </s:Body> </s:Envelope>
In the storefront when the user enters the address on the checkout address page, a request is raised to the server to validate the address. This request is handled by the GetCart handler, and from there the call moves to the Avalara tax plug-in. We customized the tax calculator to add validation of address before calculating the tax amount.
Here is the address validation part of the code in CalculateTax method:
public void CalculateTax(OriginAddress originAddress, CustomerOrder customerOrder) { LogHelper.For(this).Info(string.Format("Entering into Calculate Tax method")); var billingCountry = customerOrder.BTCountry; var shippingCountry = customerOrder.ShipTo.Country.Name; try { //Address Validation if (customerOrder.GetProperty("addressValidation", "").Equals("true", StringComparison.OrdinalIgnoreCase)) { customerOrder.SetProperty("addressValidation", "false"); ValidateAddress(customerOrder); return; } } //Rest of code removed to save space }
The below code shows how the request is created and the call is initiated:
protected virtual void ValidateAddress(CustomerOrder customerOrder) { //address Validation if (customerOrder.ShipTo.IsDropShip || (SiteContext.Current.UserProfileDto != null && SiteContext.Current.UserProfileDto.IsGuest) || (_regalWareSystemSettings.OrderSubmit_API_Service==PossibleAPIServiceValues.ESPRO)) { var shipToAddress = new Address { Line1 = customerOrder.STAddress1, Line2 = customerOrder.STAddress2, Line3 = customerOrder.STAddress3, City = customerOrder.STCity, Region = customerOrder.STState, PostalCode = customerOrder.STPostalCode }; object obj = DependencyLocator.Current.GetInstance<IUnitOfWorkFactory>().GetUnitOfWork().GetRepository<Country>().GetTable().FirstOrDefault(c => c.Name == customerOrder.STCountry); if (obj == null) { var shipTo = customerOrder.ShipTo; if (shipTo != null) { obj = shipTo.Country; } } var country1 = (Country)obj; object sTCountry = country1?.IsoCode2 ?? customerOrder.STCountry; shipToAddress.Country = (string)sTCountry; string str1; try { var configuredService = GetConfiguredAddressService(); var validateRequest = new ValidateRequest(); validateRequest.Address = shipToAddress; var ValidateResult = configuredService.Validate(validateRequest); if (AvalaraSettings.LogTransactions) { LogHelper.For(this).Info($"Validate Address request: {SerializeObject(validateRequest)}{Environment.NewLine} Response: {SerializeObject(ValidateResult)}", "TaxCalculator_Avalara"); } str1 = ProcessAddressAvalaraResponseMessage(ValidateResult); } catch (Exception ex) { LogHelper.For(this).Error(string.Concat("Error on validating Address on Avalara for US/CA customers for drop ship ", ex.Message), ex, "TaxCalculator_RegalWare", null); throw; } if (!string.IsNullOrEmpty(str1)) { LogHelper.For(this).Debug(str1, "Avalara Address Result, Validate: "); } } }
In the validation request call, the address is passed on and if validation is successful, we receive a result code as a return value. Otherwise, Avalara responds with an error as a result code and a list of messages specifying which part of the address is wrong. In case of errors, a specific validation exception is raised, and from there the code handles errors.
Here is the code that shows how the response is handled from the request:
private string ProcessAddressAvalaraResponseMessage(BaseResult result) { var stringBuilder = new StringBuilder(); if (result.ResultCode == SeverityLevel.Success) { return stringBuilder.ToString(); } foreach (Message message in result.Messages) { if (addressErrorMessages.Contains<string>(message.Name)) { LogHelper.For(this).Error($"{message.Name} - {message.Details}", "AddressValidation_Avalara"); throw new AddressValidateException(message.Summary.ToString()); } stringBuilder.AppendLine(string.Concat("Name: ", message.Name)); stringBuilder.AppendLine(string.Concat("Severity: ", message.Severity)); stringBuilder.AppendLine(string.Concat("Summary: ", message.Summary)); stringBuilder.AppendLine(string.Concat("Details: ", message.Details)); stringBuilder.AppendLine(string.Concat("Source: ", message.Source)); stringBuilder.AppendLine(string.Concat("RefersTo: ", message.RefersTo)); stringBuilder.AppendLine(string.Concat("HelpLink: ", message.HelpLink)); } if (result.ResultCode == SeverityLevel.Error || result.ResultCode == SeverityLevel.Exception) { throw new Exception(stringBuilder.ToString()); } return stringBuilder.ToString(); } }
The error message is captured from the exception and is passed to the calling code as a response. On the storefront the message is displayed to the user as a popup and based on our client’s requirement until the user fixes the address error, they are not allowed to move on from the address page. This way users are forced to fix the address on the storefront and that helped to resolve the incorrect tax calculation.
Conclusion
Optimizely B2B Commerce Cloud architecture is very flexible. It allowed me to quickly customize the Avalara tax plug-in to include the functionality our client was looking for. Once we deployed this solution to production, we haven’t heard any more incidents of incorrect tax calculation.