diff --git a/src/Avalara-logo.png b/src/Avalara-logo.png new file mode 100644 index 0000000..2341e29 Binary files /dev/null and b/src/Avalara-logo.png differ diff --git a/src/AvalaraAddressValidatorProvider.cs b/src/AvalaraAddressValidatorProvider.cs index 722e779..724d62d 100644 --- a/src/AvalaraAddressValidatorProvider.cs +++ b/src/AvalaraAddressValidatorProvider.cs @@ -1,292 +1,237 @@ -using Avalara.AvaTax.RestClient; -using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Ecommerce.Orders; using Dynamicweb.Ecommerce.Orders.AddressValidation; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.ResolveAddressResponse; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Service; using Dynamicweb.Extensibility.AddIns; using Dynamicweb.Extensibility.Editors; using System; -using System.IO; +using System.Linq; using System.Text; -using System.Xml.Serialization; -namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider -{ - /// - /// Avalara address validation provider - /// - [AddInName("Avalara address validation provider")] - public class AvalaraAddressValidatorProvider : AddressValidatorProvider - { - - #region Fields +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider; - [AddInParameter("Account"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] - public string Account { get; set; } - - [AddInParameter("License"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] - public string License { get; set; } +/// +/// Avalara address validation provider +/// +[AddInName("Avalara address validation provider")] +public class AvalaraAddressValidatorProvider : AddressValidatorProvider +{ + [AddInParameter("Account Id"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] + public string AccountId { get; set; } - [AddInParameter("Company Code"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] - public string CompanyCode { get; set; } + [AddInParameter("License Key"), AddInParameterEditor(typeof(TextParameterEditor), "size=80; password=true")] + public string LicenseKey { get; set; } - [AddInParameter("Address Service Url"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] - public string AddressServiceUrl { get; set; } + [AddInParameter("Validate Billing Address"), AddInParameterEditor(typeof(YesNoParameterEditor), "")] + public bool ValidateBillingAddress { get; set; } - [AddInParameter("Validate Billing Address"), AddInParameterEditor(typeof(YesNoParameterEditor), ""), AddInDescription("Create a log of the request and response from UPS")] - public bool ValidateBillingAddress { get; set; } + [AddInParameter("Validate Shipping Address"), AddInParameterEditor(typeof(YesNoParameterEditor), "")] + public bool ValidateShippingAddress { get; set; } - [AddInParameter("Validate Shipping Address"), AddInParameterEditor(typeof(YesNoParameterEditor), ""), AddInDescription("Create a log of the request and response from UPS")] - public bool ValidateShippingAddress { get; set; } + [AddInParameter("Debug"), AddInParameterEditor(typeof(YesNoParameterEditor), "infoText=Create a log of the request and response from Avalara")] + public bool Debug { get; set; } - [AddInParameter("Debug"), AddInParameterEditor(typeof(YesNoParameterEditor), ""), AddInDescription("Create a log of the request and response from UPS")] - public bool Debug { get; set; } - #endregion + [AddInParameter("Test Mode"), AddInParameterEditor(typeof(YesNoParameterEditor), "infoText=Set to use sandbox (test mode) for the API requests. Uncheck when ready for production.")] + public bool TestMode { get; set; } - public override void Validate(Order order) + public override void Validate(Order order) + { + if (ValidateBillingAddress) { - var service = PrepareAddressSvc(); + AddressLocationInfo billingAddress = GetBillingAddress(order); + var addressValidatorResult = new AddressValidatorResult(ValidatorId, AddressType.Billing); - if (ValidateBillingAddress) + try { - var billingAddress = GetBillingAddress(order); - var addressValidatorResult = new AddressValidatorResult(ValidatorId, AddressType.Billing); - - try - { - if (string.IsNullOrEmpty(billingAddress.postalCode) && string.IsNullOrEmpty(billingAddress.line1)) - { - addressValidatorResult.IsError = true; - addressValidatorResult.ErrorMessage = "Insufficient address information"; - } - else - { - var validateResult = ValidateAddress(service, billingAddress, AddressType.Billing); - - if (validateResult.messages is null) - { - var validAddress = validateResult.validatedAddresses[0]; - addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine1, order.CustomerAddress, validAddress.line1); - addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine2, order.CustomerAddress2, validAddress.line2); - addressValidatorResult.CheckAddressField(AddressFieldType.City, order.CustomerCity, validAddress.city); - addressValidatorResult.CheckAddressField(AddressFieldType.Region, order.CustomerRegion, validAddress.region); - addressValidatorResult.CheckAddressField(AddressFieldType.ZipCode, order.CustomerZip, validAddress.postalCode); - } - else - { - addressValidatorResult.IsError = true; - addressValidatorResult.ErrorMessage = GetErrorMessage(validateResult); - } - } - } - catch (Exception exception) + if (string.IsNullOrEmpty(billingAddress.PostalCode) && string.IsNullOrEmpty(billingAddress.Line1)) { addressValidatorResult.IsError = true; - addressValidatorResult.ErrorMessage = "AvaTax threw an exception while validating address: " + exception.Message; + addressValidatorResult.ErrorMessage = "Insufficient address information"; } - - if (addressValidatorResult.IsError || addressValidatorResult.AddressFields.Count > 0) + else { - order.AddressValidatorResults.Add(addressValidatorResult); - } - } - - if (ValidateShippingAddress) - { - var deliveryAddress = GetDeliveryAddress(order); - var addressValidatorResult = new AddressValidatorResult(ValidatorId, AddressType.Delivery); + ResolveAddressResponse validateResult = ValidateAddress(billingAddress, AddressType.Billing); - try - { - if (string.IsNullOrEmpty(deliveryAddress.postalCode) && string.IsNullOrEmpty(deliveryAddress.line1)) + if (validateResult.Messages is null && validateResult.ValidatedAddresses.Any()) { - addressValidatorResult.IsError = true; - addressValidatorResult.ErrorMessage = "Insufficient address information"; + ValidatedAddressInfo validAddress = validateResult.ValidatedAddresses.FirstOrDefault(); + addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine1, order.CustomerAddress ?? "", validAddress.Line1 ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine2, order.CustomerAddress2 ?? "", validAddress.Line2 ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.City, order.CustomerCity ?? "", validAddress.City ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.Region, order.CustomerRegion ?? "", validAddress.Region ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.ZipCode, order.CustomerZip ?? "", validAddress.PostalCode ?? ""); } else { - var validateResult = ValidateAddress(service, deliveryAddress, AddressType.Delivery); - - if (validateResult.messages is null) - { - var validAddress = validateResult.validatedAddresses[0]; - addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine1, order.DeliveryAddress, validAddress.line1); - addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine2, order.DeliveryAddress2, validAddress.line2); - addressValidatorResult.CheckAddressField(AddressFieldType.City, order.DeliveryCity, validAddress.city); - addressValidatorResult.CheckAddressField(AddressFieldType.Region, order.DeliveryRegion, validAddress.region); - addressValidatorResult.CheckAddressField(AddressFieldType.ZipCode, order.DeliveryZip, validAddress.postalCode); - } - else - { - addressValidatorResult.IsError = true; - addressValidatorResult.ErrorMessage = GetErrorMessage(validateResult); - } + addressValidatorResult.IsError = true; + addressValidatorResult.ErrorMessage = GetErrorMessage(validateResult); } } - catch (Exception exception) - { - addressValidatorResult.IsError = true; - addressValidatorResult.ErrorMessage = "AvaTax threw an exception while validating address: " + exception.Message; - } - - if (addressValidatorResult.IsError || addressValidatorResult.AddressFields.Count > 0) - { - order.AddressValidatorResults.Add(addressValidatorResult); - } + } + catch (Exception exception) + { + addressValidatorResult.IsError = true; + addressValidatorResult.ErrorMessage = "AvaTax threw an exception while validating address: " + exception.Message; } + if (addressValidatorResult.IsError || addressValidatorResult.AddressFields.Count > 0) + order.AddressValidatorResults.Add(addressValidatorResult); } - #region private functions - - private AddressResolutionModel ValidateAddress(AvaTaxClient addressService, AddressLocationInfo address, AddressType addressType) + if (ValidateShippingAddress) { - var validateResult = CheckIsAddressCached(address, addressType); + AddressLocationInfo deliveryAddress = GetDeliveryAddress(order); + var addressValidatorResult = new AddressValidatorResult(ValidatorId, AddressType.Delivery); - if (validateResult == null) + try { - validateResult = addressService.ResolveAddress(address.line1, address.line2, null, address.city, address.region, address.postalCode, address.country, TextCase.Mixed); - - if (Debug) + if (string.IsNullOrEmpty(deliveryAddress.PostalCode) && string.IsNullOrEmpty(deliveryAddress.Line1)) { - SaveAvaTaxLog(validateResult); + addressValidatorResult.IsError = true; + addressValidatorResult.ErrorMessage = "Insufficient address information"; } + else + { + ResolveAddressResponse validateResult = ValidateAddress(deliveryAddress, AddressType.Delivery); - CacheRateRequest(address, addressType, validateResult); + if (validateResult.Messages is null && validateResult.ValidatedAddresses.Any()) + { + var validAddress = validateResult.ValidatedAddresses.FirstOrDefault(); + addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine1, order.DeliveryAddress ?? "", validAddress.Line1 ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.AddressLine2, order.DeliveryAddress2 ?? "", validAddress.Line2 ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.City, order.DeliveryCity ?? "", validAddress.City ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.Region, order.DeliveryRegion ?? "", validAddress.Region ?? ""); + addressValidatorResult.CheckAddressField(AddressFieldType.ZipCode, order.DeliveryZip ?? "", validAddress.PostalCode ?? ""); + } + else + { + addressValidatorResult.IsError = true; + addressValidatorResult.ErrorMessage = GetErrorMessage(validateResult); + } + } } - - return validateResult; - } - private AvaTaxClient PrepareAddressSvc() - { - return new AvaTaxClient("Dynamicweb AvaTax", "1.0", "Dynamicweb 9.0", new Uri(AddressServiceUrl)).WithSecurity(Account, License); - } - - #region Cache address validator request - - private static string AddressValidatorCacheKey(int validatorId, AddressType addressType) - { - return string.Format("AddressServiceRequest_{0}_{1}", validatorId, addressType); - } - - private AddressResolutionModel CheckIsAddressCached(AddressLocationInfo address, AddressType addressType) - { - AddressResolutionModel validateResult = null; - - if ((Context.Current.Session[AddressValidatorCacheKey(ValidatorId, addressType)] != null)) + catch (Exception exception) { - var cachedRequest = (ValidateCache)Context.Current.Session[AddressValidatorCacheKey(ValidatorId, addressType)]; - - if (address.country == cachedRequest.Address.country && - address.region == cachedRequest.Address.region && - address.postalCode == cachedRequest.Address.postalCode && - address.line1 == cachedRequest.Address.line1 && - address.line2 == cachedRequest.Address.line2 && - address.line3 == cachedRequest.Address.line3) - { - validateResult = cachedRequest.ValidateResult; - } + addressValidatorResult.IsError = true; + addressValidatorResult.ErrorMessage = "AvaTax threw an exception while validating address: " + exception.Message; } - return validateResult; + if (addressValidatorResult.IsError || addressValidatorResult.AddressFields.Count > 0) + { + order.AddressValidatorResults.Add(addressValidatorResult); + } } - private void CacheRateRequest(AddressLocationInfo address, AddressType addressType, AddressResolutionModel validateResult) + } + + private ResolveAddressResponse ValidateAddress(AddressLocationInfo address, AddressType addressType) + { + var validateResult = CheckIsAddressCached(address, addressType); + + if (validateResult is null) { - Context.Current.Session[AddressValidatorCacheKey(ValidatorId, addressType)] = new ValidateCache + var service = new AvalaraService { - Address = address, - ValidateResult = validateResult + AccountId = AccountId, + LicenseKey = LicenseKey, + TestMode = TestMode, + DebugLog = Debug }; - } + validateResult = service.ResolveAddress(address); - private class ValidateCache - { - public AddressLocationInfo Address; - public AddressResolutionModel ValidateResult; + CacheRateRequest(address, addressType, validateResult); } - #endregion + return validateResult; + } - #endregion + private static string AddressValidatorCacheKey(int validatorId, AddressType addressType) + => $"AddressServiceRequest_{validatorId}_{addressType}"; - #region public static functions + private ResolveAddressResponse CheckIsAddressCached(AddressLocationInfo address, AddressType addressType) + { + ResolveAddressResponse validateResult = null; - public static AddressLocationInfo GetBillingAddress(Order order) + if (Context.Current.Session[AddressValidatorCacheKey(ValidatorId, addressType)] is not null) { - return new AddressLocationInfo + var cachedRequest = (ValidateCache)Context.Current.Session[AddressValidatorCacheKey(ValidatorId, addressType)]; + + if (string.Equals(address.Country, cachedRequest.Address.Country, StringComparison.OrdinalIgnoreCase) && + string.Equals(address.Region, cachedRequest.Address.Region, StringComparison.OrdinalIgnoreCase) && + string.Equals(address.PostalCode, cachedRequest.Address.PostalCode, StringComparison.OrdinalIgnoreCase) && + string.Equals(address.Line1, cachedRequest.Address.Line1, StringComparison.OrdinalIgnoreCase) && + string.Equals(address.Line2, cachedRequest.Address.Line2, StringComparison.OrdinalIgnoreCase) && + string.Equals(address.Line3, cachedRequest.Address.Line3, StringComparison.OrdinalIgnoreCase)) { - line1 = order.CustomerAddress, - line2 = order.CustomerAddress2, - city = order.CustomerCity, - region = order.CustomerRegion, - postalCode = order.CustomerZip, - country = order.CustomerCountryCode - }; + validateResult = cachedRequest.ValidateResult; + } } - public static AddressLocationInfo GetDeliveryAddress(Order order) - { - return new AddressLocationInfo - { - line1 = order.DeliveryAddress, - line2 = order.DeliveryAddress2, - city = order.DeliveryCity, - region = order.DeliveryRegion, - postalCode = order.DeliveryZip, - country = order.DeliveryCountryCode - }; - } + return validateResult; + } - public static AddressLocationInfo GetOriginAddress(AvalaraTaxProvider taxProvider) + private void CacheRateRequest(AddressLocationInfo address, AddressType addressType, ResolveAddressResponse validateResult) + { + Context.Current.Session[AddressValidatorCacheKey(ValidatorId, addressType)] = new ValidateCache { - return new AddressLocationInfo - { - line1 = taxProvider.StreetAddress, - line2 = taxProvider.StreetAddress2, - city = taxProvider.City, - region = taxProvider.Region, - postalCode = taxProvider.PostalCode, - country = taxProvider.Country - }; - } + Address = address, + ValidateResult = validateResult + }; + } - #endregion + private class ValidateCache + { + public AddressLocationInfo Address; + public ResolveAddressResponse ValidateResult; + } - #region SaveAvaTaxLog + internal static AddressLocationInfo GetBillingAddress(Order order) => new() + { + Line1 = order.CustomerAddress, + Line2 = order.CustomerAddress2, + City = order.CustomerCity, + Region = order.CustomerRegion, + PostalCode = order.CustomerZip, + Country = order.CustomerCountryCode + }; + + internal static AddressLocationInfo GetDeliveryAddress(Order order) => new() + { + Line1 = order.DeliveryAddress, + Line2 = order.DeliveryAddress2, + City = order.DeliveryCity, + Region = order.DeliveryRegion, + PostalCode = order.DeliveryZip, + Country = order.DeliveryCountryCode + }; + + internal static AddressLocationInfo GetOriginAddress(AvalaraTaxProvider taxProvider) => new() + { + Line1 = taxProvider.StreetAddress, + Line2 = taxProvider.StreetAddress2, + City = taxProvider.City, + Region = taxProvider.Region, + PostalCode = taxProvider.PostalCode, + Country = taxProvider.Country + }; + + private string GetErrorMessage(ResolveAddressResponse validateResult) + { + var errMessages = new StringBuilder(); - private string GetErrorMessage(AddressResolutionModel validateResult) + if (validateResult.Messages?.Any() is true) { - var errMessages = new StringBuilder(); - - if (validateResult.messages?.Count > 0) + foreach (var message in validateResult.Messages) { - foreach (var message in validateResult.messages) - { - errMessages.AppendLine($"Details: {message.details}"); - errMessages.AppendLine($"RefersTo: {message.refersTo}"); - errMessages.AppendLine($"Severity: {message.severity}"); - errMessages.AppendLine($"Source: {message.source}"); - errMessages.AppendLine($"Summary: {message.summary}"); - } + errMessages.AppendLine($"Details: {message.Details}"); + errMessages.AppendLine($"RefersTo: {message.RefersTo}"); + errMessages.AppendLine($"Severity: {message.Severity}"); + errMessages.AppendLine($"Source: {message.Source}"); + errMessages.AppendLine($"Summary: {message.Summary}"); } - - return errMessages.ToString(); } - private void SaveAvaTaxLog(AddressResolutionModel validateRequest) - { - try - { - var serializer = new XmlSerializer(typeof(AddressResolutionModel)); - var writer = new StringWriter(); - serializer.Serialize(writer, validateRequest); - - SaveLog(writer.ToString()); - } - catch (Exception err) - { - SaveLog(err.ToString()); - } - } - #endregion + return errMessages.ToString(); } } diff --git a/src/AvalaraTaxProvider.cs b/src/AvalaraTaxProvider.cs index 5daafbd..1dc1179 100644 --- a/src/AvalaraTaxProvider.cs +++ b/src/AvalaraTaxProvider.cs @@ -1,26 +1,22 @@ -using Avalara.AvaTax.RestClient; -using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Ecommerce.Orders; using Dynamicweb.Ecommerce.Prices; using Dynamicweb.Ecommerce.Products; using Dynamicweb.Ecommerce.Products.Taxes; -using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model; -using Dynamicweb.Extensibility; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.VoidTransaction; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Notifications; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Service; using Dynamicweb.Extensibility.AddIns; using Dynamicweb.Extensibility.Editors; using Dynamicweb.Extensibility.Notifications; using Dynamicweb.Security.UserManagement; using Dynamicweb.Security.UserManagement.Common.CustomFields; using Dynamicweb.Security.UserManagement.Common.SystemFields; +using Microsoft.CodeAnalysis; using System; -using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; using System.Text; -using System.Text.Json; -using System.Xml.Serialization; namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider { @@ -28,52 +24,30 @@ namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider /// Avalara tax provider /// [AddInName("Avalara tax provider")] - public class AvalaraTaxProvider : TaxProvider, IDropDownOptions + public class AvalaraTaxProvider : TaxProvider, IParameterOptions { /// /// Gets the names for ItemCode and TaxCode field. /// - private const string ItemCodeFieldName = "ItemCode"; - private const string TaxCodeFieldName = "TaxCode"; - private const string ExemptionNumberFieldName = "ExemptionNumber"; - private const string EntityUseCodeFieldName = "EntityUseCode"; + internal const string ItemCodeFieldName = "ItemCode"; + internal const string TaxCodeFieldName = "TaxCode"; + internal const string ExemptionNumberFieldName = "ExemptionNumber"; + internal const string EntityUseCodeFieldName = "EntityUseCode"; public const string BeforeTaxCalculation = "Ecom7CartBeforeTaxCalculation"; public const string BeforeTaxCommit = "Ecom7CartBeforeTaxCommit"; public const string OnGetCustomerCode = "Ecom7CartAvalaraOnGetCustomerCode"; - private OrderDebuggingInfoService _orderDebuggingInfoService = new OrderDebuggingInfoService(); - private enum TransactionType - { - Calculate, - Commit, - Cancel, - Adjust, - ProductReturns - } - - private enum CustomerCodeSource - { - OrderCustomerAccessUserId, - OrderCustomerNumber, - AccessUserExternalId - } - - private Order originalOrder = null; - - #region Fields + private OrderDebuggingInfoService _orderDebuggingInfoService = new OrderDebuggingInfoService(); - [AddInParameter("Account"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] - public string Account { get; set; } + [AddInParameter("Account Id"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] + public string AccountId { get; set; } - [AddInParameter("License"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] - public string License { get; set; } + [AddInParameter("License Key"), AddInParameterEditor(typeof(TextParameterEditor), "size=80; password=true")] + public string LicenseKey { get; set; } [AddInParameter("Company Code"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] public string CompanyCode { get; set; } - [AddInParameter("Tax Service Url"), AddInParameterEditor(typeof(TextParameterEditor), "size=80")] - public string TaxServiceUrl { get; set; } - [AddInParameter("Origination Street Address"), AddInParameterEditor(typeof(TextParameterEditor), "")] public string StreetAddress { get; set; } @@ -89,44 +63,36 @@ private enum CustomerCodeSource [AddInParameter("Origination Zip Code"), AddInParameterEditor(typeof(TextParameterEditor), "")] public string PostalCode { get; set; } - public string Country = "US"; - [AddInParameter("Tax Code for Shipping"), AddInParameterEditor(typeof(TextParameterEditor), "")] - public string TaxCodeShipping { get; set; } - - [AddInParameter("Boundary level"), AddInParameterEditor(typeof(DropDownParameterEditor), "none=false; SortBy=Value")] - public string BoundaryLevel { get; set; } + public string TaxCodeShipping { get; set; } = "FR020100"; [AddInParameter("Get customer code from"), AddInParameterEditor(typeof(DropDownParameterEditor), "none=false; SortBy=Value")] - public string GetCustomerCodeFrom { get; set; } + public string GetCustomerCodeFrom { get; set; } = nameof(CustomerCodeSource.OrderCustomerAccessUserId); [AddInParameter("Enable Commit"), AddInParameterEditor(typeof(YesNoParameterEditor), "")] - public bool EnableCommit { get; set; } + public bool EnableCommit { get; set; } = true; [AddInParameter("Don't use in product catalog"), AddInParameterEditor(typeof(YesNoParameterEditor), "")] public bool DontUseInProductCatalog { get; set; } - [AddInParameter("Don’t calculate taxes if {Exemption number} is set"), AddInParameterEditor(typeof(YesNoParameterEditor), "")] - public Boolean DontUseIfExemptionNumberIsSet { get; set; } + [AddInParameter("Don’t calculate taxes if Exemption number is set"), AddInParameterEditor(typeof(YesNoParameterEditor), "")] + public bool DontUseIfExemptionNumberIsSet { get; set; } - [AddInParameter("Debug"), AddInParameterEditor(typeof(YesNoParameterEditor), ""), AddInDescription("Create a log of the request and response from UPS")] + [AddInParameter("Debug"), AddInParameterEditor(typeof(YesNoParameterEditor), ""), AddInDescription("Create a log of the request and response from Avalara")] public bool Debug { get; set; } - #endregion + [AddInParameter("Test mode"), AddInParameterEditor(typeof(YesNoParameterEditor), "infoText=Set to use sandbox (test mode) for the API requests. Uncheck when ready for production.")] + public bool TestMode { get; set; } - /// - /// Default constructor - /// - public AvalaraTaxProvider() + public string Country = "US"; + + private AvalaraService GetService() => new() { - if (Context.Current == null || Context.Current.Request.Form.Count == 0) - { - EnableCommit = true; - BoundaryLevel = "Zip9"; - TaxCodeShipping = "FR020100"; // Avalara System TaxCode for SHIPPING - } - GetCustomerCodeFrom = nameof(CustomerCodeSource.OrderCustomerAccessUserId); - } + AccountId = AccountId, + LicenseKey = LicenseKey, + TestMode = TestMode, + DebugLog = Debug + }; /// /// Adds order lines to order @@ -139,32 +105,19 @@ public override void AddTaxOrderLinesToOrder(Order order) NotificationManager.Notify(BeforeTaxCalculation, notificationArgs); if (notificationArgs.Cancel) - { return; - } if (!IsTaxableOrder(order)) - { return; - } try { - var taxResult = GetTaxes(order, TransactionType.Calculate); + CreateTransactionResponse taxResult = GetService().CreateCalculateTransaction(order, this); - if (Debug) - { - SaveAvaTaxLog(taxResult); - } - - if (taxResult.messages?.Count > 0) - { + if (taxResult.Messages?.Any() is true) order.TaxProviderErrors.Add(GetErrorMessage(taxResult)); - } else - { GetOrderLinesFromTaxResult(order, taxResult); - } } catch (Exception err) { @@ -184,41 +137,30 @@ public override void CommitTaxes(Order order) NotificationManager.Notify(BeforeTaxCommit, notificationArgs); if (notificationArgs.Cancel) - { return; - } if (!order.Complete || !IsTaxableOrder(order)) - { return; - } try { - var taxResult = GetTaxes(order, TransactionType.Commit); + CreateTransactionResponse taxResult = GetService().CreateCommitTransaction(order, this); - if (Debug) - { - SaveAvaTaxLog(taxResult); - } - string message = string.Format("Commited with ResultCode ({0})", taxResult.code); - if (taxResult.messages is null) + string message = $"Commited with ResultCode ({taxResult.Code})"; + if (taxResult.Messages is null) { if (EnableCommit) { - message += string.Format("; TransactionId #{0}", taxResult.code); - order.TaxTransactionNumber = taxResult.code; + message += $"; TransactionId #{taxResult.Code}"; + order.TaxTransactionNumber = taxResult.Code; Services.Orders.Save(order); } else - { - message += string.Format("; Commit is disabled"); - } + message += "; Commit is disabled"; } else - { message += GetErrorMessage(taxResult); - } + new OrderDebuggingInfoService().Save(order, message, "AvaTax"); } catch (Exception err) @@ -229,408 +171,163 @@ public override void CommitTaxes(Order order) #region get taxes - private TransactionModel GetTaxes(Order order, TransactionType transactionType) - { - var taxRequest = PrepareTaxRequest(order, transactionType).Create(); - - if (Debug) - { - SaveAvaTaxLog(taxRequest); - } - - return taxRequest; - } - - private AvaTaxClient PrepareTaxSvc() - { - return new AvaTaxClient("Dynamicweb AvaTax", "1.0", "Dynamicweb 9.0", new Uri(TaxServiceUrl)).WithSecurity(Account, License); - } - private T PostToAvalara(string method, string jsonObject) - { - string url = $"{TaxServiceUrl}/api/v2/"; - using (var client = new HttpClient()) - { - string authenticationScheme = "Basic"; - string authenticationParameter = Convert.ToBase64String(Encoding.Default.GetBytes($"{Account}:{License}")); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authenticationScheme, authenticationParameter); - var content = new StringContent(jsonObject); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - using (var response = client.PostAsync(url + method, content).GetAwaiter().GetResult()) - { - string responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - return JsonSerializer.Deserialize(responseText); - } - } - } - - /// - /// "FR020100" - Avalara System TaxCode for SHIPPING - /// - private TransactionBuilder PrepareTaxRequest(Order order, TransactionType transactionType) - { - TransactionBuilder result; - if (transactionType == TransactionType.Commit) - { - result = new TransactionBuilder(PrepareTaxSvc(), CompanyCode, DocumentType.SalesInvoice, GetCustomerCode(order)); - result.WithCommit(); - result.WithDate(order.Date); - result.WithReferenceCode(order.Id); - } - else if (transactionType == TransactionType.Calculate) - { - result = new TransactionBuilder(PrepareTaxSvc(), CompanyCode, DocumentType.SalesOrder, GetCustomerCode(order)); - result.WithDate(DateTime.Now); - result.WithReferenceCode(order.Id); - } - else if (transactionType == TransactionType.Adjust) - { - result = new TransactionBuilder(PrepareTaxSvc(), CompanyCode, DocumentType.SalesInvoice, GetCustomerCode(order)); - if (!string.IsNullOrEmpty(order.TaxTransactionNumber) && EnableCommit) - { - result.WithCommit(); - } - result.WithDate(DateTime.Now); - result.WithTaxOverride(TaxOverrideType.TaxDate, "Adjust", 0, order.Date); - result.WithReferenceCode(order.Id); - } - else if (transactionType == TransactionType.ProductReturns) - { - result = new TransactionBuilder(PrepareTaxSvc(), CompanyCode, DocumentType.ReturnInvoice, GetCustomerCode(order)); - if (!string.IsNullOrEmpty(originalOrder.TaxTransactionNumber) && EnableCommit) - { - result.WithCommit(); - } - result.WithDate(order.Date); - result.WithReferenceCode(originalOrder.Id); - result.WithTaxOverride(TaxOverrideType.TaxDate, "Return", 0, originalOrder.Date); - } - else - { - throw new Exception(string.Format("Unknown transaction type: {0}", transactionType)); - } - - result.WithCurrencyCode(order.CurrencyCode); - - if (order.CustomerAccessUserId != 0) - { - var customer = User.GetUserByID(order.CustomerAccessUserId); - - foreach (var fieldValue in customer.SystemFieldValues) - { - if (fieldValue.SystemField.Name == ExemptionNumberFieldName && fieldValue.Value != null) - { - result.WithExemptionNumber(fieldValue.Value.ToString()); - } - else if (fieldValue.SystemField.Name == EntityUseCodeFieldName && fieldValue.Value != null) - { - result.WithUsageType(fieldValue.Value.ToString()); - } - } - } - - AddressLocationInfo originAddress = AvalaraAddressValidatorProvider.GetOriginAddress(this); - result.WithAddress(TransactionAddressType.ShipFrom, originAddress.line1, originAddress.line2, null, originAddress.city, originAddress.region, originAddress.postalCode, originAddress.country); - - var destinationAddress = new AddressLocationInfo(); - if (!string.IsNullOrEmpty(order.DeliveryZip)) - { - destinationAddress = AvalaraAddressValidatorProvider.GetDeliveryAddress(order); - } - else - { - destinationAddress = AvalaraAddressValidatorProvider.GetBillingAddress(order); - } - if (string.IsNullOrEmpty(destinationAddress.postalCode)) - { - throw new Exception("Make sure that the address is provided with a zip code."); - } - result.WithAddress(TransactionAddressType.ShipTo, destinationAddress.line1, destinationAddress.line2, null, destinationAddress.city, destinationAddress.region, destinationAddress.postalCode, destinationAddress.country); - - int index = 0; - decimal orderDiscount = 0M; - CreateTransactionModel transactionModel = result.GetCreateTransactionModel(); - var priceContext = new PriceContext(order.Currency, order.VatCountry); - - foreach (var orderLine in order.OrderLines) - { - if (IsTaxableType(orderLine) || orderLine.HasType(OrderLineType.PointProduct)) - { - if (orderLine.Product != null) - { - var line = GetTaxLine(order, orderLine, index++, destinationAddress, originAddress); - transactionModel.lines.Add(line); - if (orderLine.HasType(OrderLineType.PointProduct)) - { - orderDiscount += (-Convert.ToDecimal(orderLine.Product.GetPrice(priceContext).PriceWithoutVAT)); - } - } - } - else if (orderLine.HasType(OrderLineType.Discount) && string.IsNullOrEmpty(orderLine.GiftCardCode)) - { - orderDiscount += Convert.ToDecimal(orderLine.Price.PriceWithoutVAT); - } - } - - orderDiscount = Math.Abs(orderDiscount); - if (orderDiscount > 0M) - { - foreach (LineItemModel line in transactionModel.lines) - { - if (line.taxCode != TaxCodeShipping) - { - line.discounted = true; - } - } - result.WithDiscountAmount(orderDiscount); - } - var shippingLine = GetShippingTaxLine(order, destinationAddress, originAddress); - - if (shippingLine.amount > 0) - { - transactionModel.lines.Add(shippingLine); - } - - return result; - } - - private string GetCustomerCode(Order order) - { - var notificationArgs = new OnGetCustomerCodeArgs { Order = order }; - NotificationManager.Notify(OnGetCustomerCode, notificationArgs); - if (!string.IsNullOrEmpty(notificationArgs.CustomerCode)) - { - return notificationArgs.CustomerCode; - } - - if (!string.IsNullOrEmpty(GetCustomerCodeFrom)) - { - switch (GetCustomerCodeFrom) - { - case nameof(CustomerCodeSource.OrderCustomerAccessUserId): - return order.CustomerAccessUserId.ToString(); - case nameof(CustomerCodeSource.OrderCustomerNumber): - return order.CustomerNumber; - case nameof(CustomerCodeSource.AccessUserExternalId): - if (order.CustomerAccessUserId > 0) - { - var customer = User.GetUserByID(order.CustomerAccessUserId); - return customer != null ? customer.ExternalID : string.Empty; - } - return string.Empty; - default: - throw new Exception("Unsupported option: " + GetCustomerCodeFrom); - } - } - return order.CustomerAccessUserId.ToString(); - } - - private LineItemModel GetTaxLine(Order order, OrderLine orderLine, int index, AddressLocationInfo destinationAddress, AddressLocationInfo originAddress) - { - LineItemModel line = new LineItemModel(); - var priceContext = new PriceContext(order.Currency, order.VatCountry); - var price = (orderLine.HasType(OrderLineType.PointProduct)) ? orderLine.Product.GetPrice(priceContext) : GetProductPriceWithoutDiscounts(orderLine); - line.amount = Convert.ToDecimal(price.PriceWithoutVAT); - line.description = orderLine.ProductName; - line.addresses = new AddressesModel - { - shipTo = destinationAddress, - shipFrom = originAddress - }; - - line.number = string.IsNullOrEmpty(orderLine.Id) ? index.ToString() : orderLine.Id; - line.quantity = Math.Abs((decimal)orderLine.Quantity); - try - { - line.itemCode = Services.Products.GetProductFieldValue(orderLine.Product, ItemCodeFieldName).ToString(); - line.taxCode = Services.Products.GetProductFieldValue(orderLine.Product, TaxCodeFieldName).ToString(); - } - catch (ArgumentException) - { - VerifyCustomFields(); - } - - return line; - } - - /// - /// "FR020100" - Avalara System TaxCode for SHIPPING - /// - private LineItemModel GetShippingTaxLine(Order order, AddressLocationInfo destinationAddress, AddressLocationInfo originAddress) - { - LineItemModel line = new LineItemModel(); - line.amount = Convert.ToDecimal(order.ShippingFee.PriceWithoutVAT); - - line.description = "SHIPPING"; - line.addresses = new AddressesModel - { - shipTo = destinationAddress, - shipFrom = originAddress - }; - - line.number = ShippingCode; - line.taxCode = TaxCodeShipping; - - return line; - } - - private void GetOrderLinesFromTaxResult(Order order, TransactionModel taxResult) + private void GetOrderLinesFromTaxResult(Order order, CreateTransactionResponse taxResult) { var newOrderLines = new OrderLineCollection(order); - if (taxResult.totalTax != 0) + if (taxResult.TotalTax != 0) { - foreach (var taxLine in taxResult.lines) + foreach (TransactionLine taxLine in taxResult.Lines) { var taxDetailNamesAndCount = new Dictionary(); - foreach (var taxDetail in taxLine.details) + foreach (TransactionLineDetail taxDetail in taxLine.Details) { - if (!taxDetailNamesAndCount.ContainsKey(taxDetail.taxName)) - { - taxDetailNamesAndCount.Add(taxDetail.taxName, 1); - } + if (!taxDetailNamesAndCount.ContainsKey(taxDetail.TaxName)) + taxDetailNamesAndCount.Add(taxDetail.TaxName, 1); else - { - taxDetailNamesAndCount[taxDetail.taxName] += 1; - } - + taxDetailNamesAndCount[taxDetail.TaxName] += 1; } - foreach (var taxDetail in taxLine.details) + foreach (TransactionLineDetail taxDetail in taxLine.Details) { - if (taxDetail.tax != 0M) - { - var taxOrderLine = new OrderLine(order); - taxOrderLine.Date = DateTime.Now; - taxOrderLine.Modified = DateTime.Now; + if (taxDetail.Tax == 0) + continue; - taxOrderLine.ProductNumber = string.Format("Tax Id# {0}", taxResult.id); - var taxName = taxDetail.taxName; - if (taxDetailNamesAndCount.ContainsKey(taxDetail.taxName) && taxDetailNamesAndCount[taxDetail.taxName] > 1 && !string.IsNullOrEmpty(taxDetail.jurisName)) - { - taxName += " (" + taxDetail.jurisName + ")"; - } - taxOrderLine.ProductName = taxName; - taxOrderLine.ProductVariantText = Name; - taxOrderLine.Order = order; - taxOrderLine.OrderId = order.Id; - taxOrderLine.Quantity = 1; - - // Info: Set price - should be before setting Type - Services.OrderLines.SetUnitPrice(taxOrderLine, Convert.ToDouble(taxDetail.tax), false); - if (!order.Calculate) - { - Services.OrderLines.SetUnitPrice(taxOrderLine, taxOrderLine.UnitPrice, true); - } + var taxOrderLine = new OrderLine(order); + taxOrderLine.Date = DateTime.Now; + taxOrderLine.Modified = DateTime.Now; - taxOrderLine.OrderLineType = OrderLineType.Tax; - taxOrderLine.ParentLineId = taxLine.lineNumber; + taxOrderLine.ProductNumber = string.Format("Tax Id# {0}", taxResult.Id); + string taxName = taxDetail.TaxName; + if (taxDetailNamesAndCount.ContainsKey(taxDetail.TaxName) && taxDetailNamesAndCount[taxDetail.TaxName] > 1 && !string.IsNullOrEmpty(taxDetail.JurisName)) + taxName += $" ({taxDetail.JurisName})"; - newOrderLines.Add(taxOrderLine); - } + taxOrderLine.ProductName = taxName; + taxOrderLine.ProductVariantText = Name; + taxOrderLine.Order = order; + taxOrderLine.OrderId = order.Id; + taxOrderLine.Quantity = 1; + + // Info: Set price - should be before setting Type + Services.OrderLines.SetUnitPrice(taxOrderLine, Convert.ToDouble(taxDetail.Tax), false); + if (!order.Calculate) + Services.OrderLines.SetUnitPrice(taxOrderLine, taxOrderLine.UnitPrice, true); + + taxOrderLine.OrderLineType = OrderLineType.Tax; + taxOrderLine.ParentLineId = taxLine.LineNumber; + + newOrderLines.Add(taxOrderLine); } } } foreach (var orderline in newOrderLines) - { order.OrderLines.Add(orderline, false); - } } private bool IsTaxableOrder(Order order) { - var ret = order.OrderLines.Any(ol => (IsTaxableType(ol) || ol.HasType(OrderLineType.PointProduct)) && ol.Product != null); + bool hasTaxableOrderLines = order.OrderLines.Any(ol => (IsTaxableType(ol) || ol.HasType(OrderLineType.PointProduct)) && ol.Product is not null); - if (ret && DontUseIfExemptionNumberIsSet && order.CustomerAccessUserId != 0) + if (hasTaxableOrderLines && DontUseIfExemptionNumberIsSet && order.CustomerAccessUserId != 0) { - var customer = User.GetUserByID(order.CustomerAccessUserId); - var exemptionNumberField = customer.SystemFieldValues.FirstOrDefault(f => f.SystemField.Name == ExemptionNumberFieldName); + User customer = UserManagementServices.Users.GetUserById(order.CustomerAccessUserId); + SystemFieldValue exemptionNumberField = customer.SystemFieldValues.FirstOrDefault(fieldValue => fieldValue.SystemField.Name.Equals(ExemptionNumberFieldName, StringComparison.Ordinal)); - if (exemptionNumberField != null && exemptionNumberField.Value != null && !string.IsNullOrEmpty(exemptionNumberField.Value.ToString())) - { - ret = false; - } + if (!string.IsNullOrWhiteSpace(exemptionNumberField?.Value?.ToString() ?? "")) + hasTaxableOrderLines = false; } - return ret; + return hasTaxableOrderLines; } #endregion - Hashtable IDropDownOptions.GetOptions(string name) + public IEnumerable GetParameterOptions(string parameterName) { - var options = new Hashtable(); + try + { + switch (parameterName) + { + case "Origination State": + return + [ + new("Alabama", "AL"), + new("Alaska", "AK"), + new("Arizona", "AZ"), + new("Arkansas", "AR"), + new("California", "CA"), + new("Colorado", "CO"), + new("Connecticut", "CT"), + new("Delaware", "DE"), + new("District of Columbia", "DC"), + new("Florida", "FL"), + new("Georgia", "GA"), + new("Hawaii", "HI"), + new("Idaho", "ID"), + new("Illinois", "IL"), + new("Indiana", "IN"), + new("Iowa", "IA"), + new("Kansas", "KS"), + new("Kentucky", "KY"), + new("Louisiana", "LA"), + new("Maine", "ME"), + new("Maryland", "MD"), + new("Massachusetts", "MA"), + new("Michigan", "MI"), + new("Minnesota", "MN"), + new("Mississippi", "MS"), + new("Missouri", "MO"), + new("Montana", "MT"), + new("Nebraska", "NE"), + new("Nevada", "NV"), + new("New Hampshire", "NH"), + new("New Jersey", "NJ"), + new("New Mexico", "NM"), + new("New York", "NY"), + new("North Carolina", "NC"), + new("North Dakota", "ND"), + new("Ohio", "OH"), + new("Oklahoma", "OK"), + new("Oregon", "OR"), + new("Pennsylvania", "PA"), + new("Rhode Island", "RI"), + new("South Carolina", "SC"), + new("South Dakota", "SD"), + new("Tennessee", "TN"), + new("Texas", "TX"), + new("Utah", "UT"), + new("Vermont", "VT"), + new("Virginia", "VA"), + new("Washington", "WA"), + new("West Virginia", "WV"), + new("Wisconsin", "WI"), + new("Wyoming", "WY") + ]; + case "Boundary level": + return + [ + new("Address", "Address"), + new("Zip9", "Zip9"), + new("Zip5", "Zip5") + ]; + case "Get customer code from": + return + [ + new("Access User ID", CustomerCodeSource.OrderCustomerAccessUserId), + new("Customer Number", CustomerCodeSource.OrderCustomerNumber), + new("External ID", CustomerCodeSource.AccessUserExternalId) + ]; - switch (name) + default: + throw new ArgumentException(string.Format("Unknown dropdown name: '{0}'", parameterName)); + } + } + catch (Exception ex) { - case "Origination State": - options.Add("AL", "Alabama"); - options.Add("AK", "Alaska"); - options.Add("AZ", "Arizona"); - options.Add("AR", "Arkansas"); - options.Add("CA", "California"); - options.Add("CO", "Colorado"); - options.Add("CT", "Connecticut"); - options.Add("DE", "Delaware"); - options.Add("DC", "District of Columbia"); - options.Add("FL", "Florida"); - options.Add("GA", "Georgia"); - options.Add("HI", "Hawaii"); - options.Add("ID", "Idaho"); - options.Add("IL", "Illinois"); - options.Add("IN", "Indiana"); - options.Add("IA", "Iowa"); - options.Add("KS", "Kansas"); - options.Add("KY", "Kentucky"); - options.Add("LA", "Louisiana"); - options.Add("ME", "Maine"); - options.Add("MD", "Maryland"); - options.Add("MA", "Massachusetts"); - options.Add("MI", "Michigan"); - options.Add("MN", "Minnesota"); - options.Add("MS", "Mississippi"); - options.Add("MO", "Missouri"); - options.Add("MT", "Montana"); - options.Add("NE", "Nebraska"); - options.Add("NV", "Nevada"); - options.Add("NH", "New Hampshire"); - options.Add("NJ", "New Jersey"); - options.Add("NM", "New Mexico"); - options.Add("NY", "New York"); - options.Add("NC", "North Carolina"); - options.Add("ND", "North Dakota"); - options.Add("OH", "Ohio"); - options.Add("OK", "Oklahoma"); - options.Add("OR", "Oregon"); - options.Add("PA", "Pennsylvania"); - options.Add("RI", "Rhode Island"); - options.Add("SC", "South Carolina"); - options.Add("SD", "South Dakota"); - options.Add("TN", "Tennessee"); - options.Add("TX", "Texas"); - options.Add("UT", "Utah"); - options.Add("VT", "Vermont"); - options.Add("VA", "Virginia"); - options.Add("WA", "Washington"); - options.Add("WV", "West Virginia"); - options.Add("WI", "Wisconsin"); - options.Add("WY", "Wyoming"); - - break; - case "Boundary level": - options.Add("Address", "Address"); - options.Add("Zip9", "Zip9"); - options.Add("Zip5", "Zip5"); - - break; - case "Get customer code from": - options.Add(CustomerCodeSource.OrderCustomerAccessUserId, "Access User ID"); - options.Add(CustomerCodeSource.OrderCustomerNumber, "Customer Number"); - options.Add(CustomerCodeSource.AccessUserExternalId, "External ID"); - break; + SaveLog($"Unhandled exception with message: {ex.Message}"); + return []; } - - return options; } #region CancelTaxRequest @@ -642,49 +339,29 @@ Hashtable IDropDownOptions.GetOptions(string name) public override void CancelTaxes(Order order) { if (string.IsNullOrEmpty(order.TaxTransactionNumber) || !EnableCommit) - { return; - } - - VoidTransactionModel voidTransactionModel = new VoidTransactionModel - { - code = VoidReasonCode.DocVoided - }; - - CancelTransactionResponse cancelTaxResult; - try { - cancelTaxResult = PostToAvalara($"companies/{CompanyCode}/transactions/{order.TaxTransactionNumber}/void", JsonSerializer.Serialize(voidTransactionModel)); - if (Debug) - { - SaveAvaTaxLog(cancelTaxResult); - } - } - catch (Exception) - { - cancelTaxResult = null; - _orderDebuggingInfoService.Save(order, "Error cancelling transaction.", "AvaTax"); - } + VoidTransactionResponse cancelTaxResult = GetService().VoidTransaction(CompanyCode, order.TaxTransactionNumber); + if (cancelTaxResult is null) + throw new ArgumentNullException("The cancel response was not deserialized correctly."); - if (cancelTaxResult != null) - { - if (cancelTaxResult.status != "Cancelled") + if (cancelTaxResult.Status != "Cancelled") { var stringBuilder = new StringBuilder(); stringBuilder.Append("Error cancelling AvaTax transaction."); - if (cancelTaxResult.messages?.Count > 0) + if (cancelTaxResult.Messages?.Any() is true) { stringBuilder.Append(" Message(s) from Gateway:"); - foreach (var message in cancelTaxResult.messages) + foreach (var message in cancelTaxResult.Messages) { - stringBuilder.Append($" Details: {message.details}"); - stringBuilder.Append($" RefersTo: {message.refersTo}"); - stringBuilder.Append($" Severity: {message.severity}"); - stringBuilder.Append($" Source: {message.source}"); - stringBuilder.Append($" Summary: {message.summary}"); + stringBuilder.Append($" Details: {message.Details}"); + stringBuilder.Append($" RefersTo: {message.RefersTo}"); + stringBuilder.Append($" Severity: {message.Severity}"); + stringBuilder.Append($" Source: {message.Source}"); + stringBuilder.Append($" Summary: {message.Summary}"); } } @@ -694,7 +371,7 @@ public override void CancelTaxes(Order order) { foreach (OrderLine orderLine in order.OrderLines) { - if (orderLine.OrderLineType == OrderLineType.Tax && orderLine.ProductVariantText == Name) + if (orderLine.OrderLineType is OrderLineType.Tax && string.Equals(orderLine.ProductVariantText, Name, StringComparison.OrdinalIgnoreCase)) { orderLine.ProductName += " - CANCELLED"; Services.OrderLines.Save(orderLine); @@ -703,6 +380,12 @@ public override void CancelTaxes(Order order) _orderDebuggingInfoService.Save(order, "Transaction was cancelled", "AvaTax"); } + + } + catch (Exception ex) + { + string message = $"Error cancelling transaction. Message: {ex.Message}"; + _orderDebuggingInfoService.Save(order, message, "AvaTax"); } } @@ -716,36 +399,29 @@ public override void CancelTaxes(Order order) /// Order instance public override void AdjustTaxes(Order order) { - if (!order.Complete) return; + if (!order.Complete) + return; try { - var taxResult = PrepareTaxRequest(order, TransactionType.Adjust).Create(); + CreateTransactionResponse taxResult = GetService().CreateAdjustTransaction(order, this); - if (Debug) - { - SaveAvaTaxLog(taxResult); - } - var message = string.Format("Taxes were adjusted with ResultCode ({0})", taxResult.code); + var message = $"Taxes were adjusted with ResultCode ({taxResult.Code})"; - if (taxResult.messages is null) + if (taxResult.Messages is null) { if (EnableCommit) { - message += string.Format("; TransactionId #{0}", taxResult.code); - order.TaxTransactionNumber = taxResult.code; + message += $"; TransactionId #{taxResult.Code}"; + order.TaxTransactionNumber = taxResult.Code; Services.Orders.Save(order); } else - { - message += string.Format("; Commit is disabled"); - } - + message += "; Commit is disabled"; } else - { message += GetErrorMessage(taxResult); - } + _orderDebuggingInfoService.Save(order, message, "AvaTax"); } catch (Exception err) @@ -762,38 +438,28 @@ public override void AdjustTaxes(Order order) public override void HandleProductReturns(Order order, Order originalOrder) { if (!order.Complete || !IsTaxableOrder(order)) - { return; - } try { - this.originalOrder = originalOrder; - var taxResult = GetTaxes(order, TransactionType.ProductReturns); + CreateTransactionResponse taxResult = GetService().CreateProductReturnsTransaction(order, this, originalOrder); - if (Debug) - { - SaveAvaTaxLog(taxResult); - } - string message = string.Format("Handle product returns with ResultCode ({0})", taxResult.code); - if (taxResult.messages is null) + string message = $"Handle product returns with ResultCode ({taxResult.Code})"; + if (taxResult.Messages is null) { GetOrderLinesFromTaxResult(order, taxResult); if (EnableCommit) { - message += string.Format("; TransactionId #{0}", taxResult.code); - order.TaxTransactionNumber = taxResult.code; + message += $"; TransactionId #{taxResult.Code}"; + order.TaxTransactionNumber = taxResult.Code; Services.Orders.Save(order); } else - { - message += string.Format("; Commit is disabled"); - } + message += "; Commit is disabled"; } else - { message += GetErrorMessage(taxResult); - } + _orderDebuggingInfoService.Save(order, message, "AvaTax"); } catch (Exception err) @@ -805,78 +471,29 @@ public override void HandleProductReturns(Order order, Order originalOrder) #endregion #region SaveAvaTaxLog - private string GetErrorMessage(TransactionModel taxResult) - { - var errMessages = new StringBuilder(); - if (taxResult.messages?.Count > 0) - { - foreach (var message in taxResult.messages) - { - errMessages.AppendLine($"Details: {message.details}"); - errMessages.AppendLine($"RefersTo: {message.refersTo}"); - errMessages.AppendLine($"Severity: {message.severity}"); - errMessages.AppendLine($"Source: {message.source}"); - errMessages.AppendLine($"Summary: {message.summary}"); - } - } - return errMessages.ToString(); - } - private void SaveAvaTaxLog(T taxRequest) + private string GetErrorMessage(CreateTransactionResponse taxResult) { - try - { - var serializer = new XmlSerializer(typeof(T)); - var writer = new StringWriter(); - serializer.Serialize(writer, taxRequest); - - SaveLog(writer.ToString()); - } - catch (Exception err) - { - SaveLog(err.ToString()); - } - } - - #endregion - - #region TestConnection - - /// - /// Tests tax service connection - /// - /// list of result information lines - public ArrayList TestConnection() - { - var taxSvc = PrepareTaxSvc(); - - var list = new ArrayList(); - try + var errMessages = new StringBuilder(); + if (taxResult.Messages?.Any() is true) { - var result = taxSvc.Ping(); - - if (!result.authenticated.GetValueOrDefault()) - { - list.Add("Ping was not successfull!"); - } - else + foreach (AvaTaxMessage message in taxResult.Messages) { - list.Add(string.Format("Is authenticated: {0}", result.authenticated.Value)); - list.Add(string.Format("Version: {0}", result.version)); + errMessages.AppendLine($"Details: {message.Details}"); + errMessages.AppendLine($"RefersTo: {message.RefersTo}"); + errMessages.AppendLine($"Severity: {message.Severity}"); + errMessages.AppendLine($"Source: {message.Source}"); + errMessages.AppendLine($"Summary: {message.Summary}"); } } - catch (Exception ex) - { - list.Add(ex.Message); - SaveLog(ex.ToString()); - } - return list; + return errMessages.ToString(); } #endregion #region VerifyCustomFields + /// /// Gets the lock object that is used to synchronize access to the queue from multiple threads. /// @@ -887,7 +504,6 @@ public ArrayList TestConnection() /// public static void VerifyCustomFields() { - lock (syncLock) { var itemCodeColl = ProductField.FindProductFieldsBySystemName(ItemCodeFieldName); @@ -923,9 +539,7 @@ public static void VerifyCustomFields() { SystemField exemptionNumberField = new SystemField(ExemptionNumberFieldName, tableName, Types.Text, ExemptionNumberFieldName); if (!systemFields.ContainsSystemField(exemptionNumberField)) - { exemptionNumberField.Save(); - } } // EntityUseCode @@ -962,36 +576,11 @@ public static void VerifyCustomFields() /// /// Verify that all needed fields for Avalara are exist and create them if not /// - public override void OnAfterSettingsSaved() - { - VerifyCustomFields(); - } + public override void OnAfterSettingsSaved() => VerifyCustomFields(); - /// - /// Before tax calculation arguments - /// - public class BeforeTaxCalculationArgs : CancelableNotificationArgs - { - public Order Order { get; set; } - } - - /// - /// Before tax commit arguments - /// - public class BeforeTaxCommitArgs : CancelableNotificationArgs - { - public Order Order { get; set; } - } - - /// - /// Args class to get the customer code to send to Avalara. - /// - public class OnGetCustomerCodeArgs : NotificationArgs - { - public Order Order { get; set; } - public string CustomerCode { get; set; } - } + internal bool IsTaxableTypeInternal(OrderLine orderLine) => IsTaxableType(orderLine); + internal PriceInfo GetProductPriceWithoutDiscountsInternal(OrderLine orderLine) => GetProductPriceWithoutDiscounts(orderLine); #region AddTaxesToProducts @@ -1003,50 +592,44 @@ public override void AddTaxesToProducts(IEnumerable products) { try { - if (DontUseInProductCatalog) return; + if (DontUseInProductCatalog) + return; - var order = PrepareOrder(products); - if (order == null || !IsTaxableOrder(order)) - { + Order order = PrepareOrder(products); + if (order is null || !IsTaxableOrder(order)) return; - } - var taxResult = GetTaxes(order, TransactionType.Calculate); - if (Debug) - { - SaveAvaTaxLog(taxResult); - } - if (taxResult.messages is null) + CreateTransactionResponse taxResult = GetService().CreateCalculateTransaction(order, this); + + if (taxResult.Messages is null) { - if (taxResult.totalTax > 0) + if (taxResult.TotalTax > 0) { - foreach (var taxLine in taxResult.lines) + foreach (var taxLine in taxResult.Lines) { - foreach (var taxDetail in taxLine.details) + foreach (var taxDetail in taxLine.Details) { - var orderLine = order.OrderLines.FirstOrDefault(line => line.Id == taxLine.lineNumber); - if (orderLine != null) - { + OrderLine orderLine = order.OrderLines.FirstOrDefault(line => line.Id.Equals(taxLine.LineNumber, StringComparison.Ordinal)); + if (orderLine is not null) products.FirstOrDefault(obj => obj.Id == orderLine.ProductId)?.TaxCollection.Add(GetTax(orderLine.Product, taxDetail)); - } } } } } } - catch (Exception) + catch (Exception ex) { - // error + SaveLog(ex.ToString()); } } - private Tax GetTax(Product product, TransactionLineDetailModel taxDetail) + private Tax GetTax(Product product, TransactionLineDetail taxDetail) { var tax = new Tax(); - tax.Name = taxDetail.taxName; + tax.Name = taxDetail.TaxName; tax.Product = product; - tax.Amount = new PriceRaw((double)taxDetail.tax, Services.Currencies.GetDefaultCurrency()); + tax.Amount = new PriceRaw((double)taxDetail.Tax, Services.Currencies.GetDefaultCurrency()); tax.CalculateVat = true; //AddVAT; return tax; @@ -1055,9 +638,9 @@ private Tax GetTax(Product product, TransactionLineDetailModel taxDetail) private Order PrepareOrder(IEnumerable products) { Order order = null; - var currentCart = Common.Context.Cart; + Order currentCart = Common.Context.Cart; - if (currentCart != null && (!string.IsNullOrEmpty(currentCart.CustomerZip) || !string.IsNullOrEmpty(currentCart.DeliveryZip))) + if (!string.IsNullOrEmpty(currentCart?.CustomerZip) || !string.IsNullOrEmpty(currentCart?.DeliveryZip)) { order = new Order(currentCart.Currency, currentCart.VatCountry, currentCart.Language); order.Id = "ProductTaxesC"; @@ -1080,9 +663,8 @@ private Order PrepareOrder(IEnumerable products) } else { - var user = User.GetCurrentUser(PagePermissionLevels.Frontend); - - if (user != null) + User user = UserContext.Current.User; + if (user is not null) { order = new Order(Common.Context.Currency, Common.Context.Country, Common.Context.Language); order.Id = "ProductTaxesU"; @@ -1093,8 +675,8 @@ private Order PrepareOrder(IEnumerable products) order.CustomerAccessUserId = user.ID; order.CustomerAccessUserUserName = user.UserName; - var defaultAddress = UserAddress.GetUserDefaultAddress(user.ID); - if (defaultAddress != null) + UserAddress defaultAddress = UserManagementServices.UserAddresses.GetDefaultAddressByUserId(user.ID); + if (defaultAddress is not null) { order.CustomerAddress = defaultAddress.Address; order.CustomerAddress2 = defaultAddress.Address2; @@ -1120,14 +702,14 @@ private Order PrepareOrder(IEnumerable products) private void PrepareOrderDetails(Order order, IEnumerable products) { - if (order != null) + if (order is null) + return; + + for (int i = 0; i < products.Count(); i++) { - var i = 0; - foreach (Product product in products) - { - var orderLine = Services.OrderLines.Create(order, product, 1d, null, null); - orderLine.Id = (i++).ToString(); - } + Product product = products.ElementAt(i); + OrderLine orderLine = Services.OrderLines.Create(order, product, 1d, null, null); + orderLine.Id = i.ToString(); } } diff --git a/src/CustomerCodeSource.cs b/src/CustomerCodeSource.cs new file mode 100644 index 0000000..b51bf7f --- /dev/null +++ b/src/CustomerCodeSource.cs @@ -0,0 +1,8 @@ +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider; + +internal enum CustomerCodeSource +{ + OrderCustomerAccessUserId, + OrderCustomerNumber, + AccessUserExternalId +} diff --git a/src/Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.csproj b/src/Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.csproj index fe49168..0ef7b17 100644 --- a/src/Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.csproj +++ b/src/Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.csproj @@ -1,7 +1,6 @@  - 10.0.0 - Alpha0001 + 10.15.0 1.0.0.0 Avalara Avalara tax provider @@ -15,18 +14,24 @@ Copyright © 2020 Dynamicweb Software A/S - net7.0 + net8.0 true true true - true + true true snupkg + Avalara-logo.png - - - - + + + + + + + True + \ + diff --git a/src/Model/Address.cs b/src/Model/Address.cs deleted file mode 100644 index 70d9edb..0000000 --- a/src/Model/Address.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model -{ - internal class Address - { - public object id { get; set; } - public object transactionId { get; set; } - public string boundaryLevel { get; set; } - public string line1 { get; set; } - public string line2 { get; set; } - public string line3 { get; set; } - public string city { get; set; } - public string region { get; set; } - public string postalCode { get; set; } - public string country { get; set; } - public int taxRegionId { get; set; } - public string latitude { get; set; } - public string longitude { get; set; } - } -} diff --git a/src/Model/CancelTransactionResponse.cs b/src/Model/CancelTransactionResponse.cs deleted file mode 100644 index a3b759e..0000000 --- a/src/Model/CancelTransactionResponse.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Avalara.AvaTax.RestClient; -using System; -using System.Collections.Generic; - -namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model -{ - internal class CancelTransactionResponse - { - public long id { get; set; } - public string code { get; set; } - public int companyId { get; set; } - public string date { get; set; } - public string paymentDate { get; set; } - public string status { get; set; } - public string type { get; set; } - public string batchCode { get; set; } - public string currencyCode { get; set; } - public string exchangeRateCurrencyCode { get; set; } - public string customerUsageType { get; set; } - public string entityUseCode { get; set; } - public string customerVendorCode { get; set; } - public string customerCode { get; set; } - public string exemptNo { get; set; } - public bool reconciled { get; set; } - public string locationCode { get; set; } - public string reportingLocationCode { get; set; } - public string purchaseOrderNo { get; set; } - public string referenceCode { get; set; } - public string salespersonCode { get; set; } - public string taxOverrideType { get; set; } - public double taxOverrideAmount { get; set; } - public string taxOverrideReason { get; set; } - public double totalAmount { get; set; } - public double totalExempt { get; set; } - public double totalDiscount { get; set; } - public double totalTax { get; set; } - public double totalTaxable { get; set; } - public double totalTaxCalculated { get; set; } - public string adjustmentReason { get; set; } - public string adjustmentDescription { get; set; } - public bool locked { get; set; } - public string region { get; set; } - public string country { get; set; } - public int version { get; set; } - public string softwareVersion { get; set; } - public long originAddressId { get; set; } - public long destinationAddressId { get; set; } - public string exchangeRateEffectiveDate { get; set; } - public double exchangeRate { get; set; } - public bool isSellerImporterOfRecord { get; set; } - public string description { get; set; } - public string email { get; set; } - public string businessIdentificationNo { get; set; } - public DateTime modifiedDate { get; set; } - public int modifiedUserId { get; set; } - public string taxDate { get; set; } - public List lines { get; set; } - public List
addresses { get; set; } - public List locationTypes { get; set; } - public List summary { get; set; } - public List messages { get; set; } - } -} diff --git a/src/Model/CreateTransactionRequest/AddressLocationInfo.cs b/src/Model/CreateTransactionRequest/AddressLocationInfo.cs new file mode 100644 index 0000000..e9e6448 --- /dev/null +++ b/src/Model/CreateTransactionRequest/AddressLocationInfo.cs @@ -0,0 +1,37 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; + +[DataContract] +internal sealed class AddressLocationInfo +{ + [DataMember(Name = "locationCode", EmitDefaultValue = false)] + public string LocationCode { get; set; } + + [DataMember(Name = "line1", EmitDefaultValue = false)] + public string Line1 { get; set; } + + [DataMember(Name = "line2", EmitDefaultValue = false)] + public string Line2 { get; set; } + + [DataMember(Name = "line3", EmitDefaultValue = false)] + public string Line3 { get; set; } + + [DataMember(Name = "city", EmitDefaultValue = false)] + public string City { get; set; } + + [DataMember(Name = "region", EmitDefaultValue = false)] + public string Region { get; set; } + + [DataMember(Name = "country", EmitDefaultValue = false)] + public string Country { get; set; } + + [DataMember(Name = "postalCode", EmitDefaultValue = false)] + public string PostalCode { get; set; } + + [DataMember(Name = "latitude", EmitDefaultValue = false)] + public double? Latitude { get; set; } + + [DataMember(Name = "longitude", EmitDefaultValue = false)] + public double? Longitude { get; set; } +} diff --git a/src/Model/CreateTransactionRequest/Addresses.cs b/src/Model/CreateTransactionRequest/Addresses.cs new file mode 100644 index 0000000..49a6fa6 --- /dev/null +++ b/src/Model/CreateTransactionRequest/Addresses.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; + +[DataContract] +internal sealed class Addresses +{ + [DataMember(Name = "shipFrom", EmitDefaultValue = false)] + public AddressLocationInfo ShipFrom { get; set; } + + [DataMember(Name = "shipTo", EmitDefaultValue = false)] + public AddressLocationInfo ShipTo { get; set; } +} diff --git a/src/Model/CreateTransactionRequest/CreateTransactionRequest.cs b/src/Model/CreateTransactionRequest/CreateTransactionRequest.cs new file mode 100644 index 0000000..8e4e4f8 --- /dev/null +++ b/src/Model/CreateTransactionRequest/CreateTransactionRequest.cs @@ -0,0 +1,49 @@ +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.Enums; +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; + +[DataContract] +internal sealed class CreateTransactionRequest +{ + [DataMember(Name = "lines", IsRequired = true)] + public List Lines { get; set; } = []; + + [DataMember(Name = "type", EmitDefaultValue = false)] + public string Type { get; set; } + + [DataMember(Name = "companyCode", EmitDefaultValue = false)] + public string CompanyCode { get; set; } + + [DataMember(Name = "date", IsRequired = true)] + public DateTime Date { get; set; } + + [DataMember(Name = "customerCode", IsRequired = true)] + public string CustomerCode { get; set; } + + [DataMember(Name = "customerUsageType", EmitDefaultValue = false)] + public string CustomerUsageType { get; set; } + + [DataMember(Name = "discount", EmitDefaultValue = false)] + public double? Discount { get; set; } + + [DataMember(Name = "exemptionNo", EmitDefaultValue = false)] + public string ExemptionNumber { get; set; } + + [DataMember(Name = "addresses", EmitDefaultValue = false)] + public Addresses Addresses { get; set; } + + [DataMember(Name = "referenceCode", EmitDefaultValue = false)] + public string ReferenceCode { get; set; } + + [DataMember(Name = "commit", EmitDefaultValue = false)] + public bool? Commit { get; set; } + + [DataMember(Name = "taxOverride", EmitDefaultValue = false)] + public TaxOverride TaxOverride { get; set; } + + [DataMember(Name = "currencyCode", EmitDefaultValue = false)] + public string CurrencyCode { get; set; } +} diff --git a/src/Model/CreateTransactionRequest/LineItem.cs b/src/Model/CreateTransactionRequest/LineItem.cs new file mode 100644 index 0000000..270a330 --- /dev/null +++ b/src/Model/CreateTransactionRequest/LineItem.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; + +[DataContract] +internal sealed class LineItem +{ + [DataMember(Name = "number", EmitDefaultValue = false)] + public string Number { get; set; } + + [DataMember(Name = "quantity", EmitDefaultValue = false)] + public double? Quantity { get; set; } + + [DataMember(Name = "amount", IsRequired = true)] + public double Amount { get; set; } + + [DataMember(Name = "addresses", EmitDefaultValue = false)] + public Addresses Addresses { get; set; } + + [DataMember(Name = "taxCode", EmitDefaultValue = false)] + public string TaxCode { get; set; } + + [DataMember(Name = "itemCode", EmitDefaultValue = false)] + public string ItemCode { get; set; } + + [DataMember(Name = "discounted", EmitDefaultValue = false)] + public bool? Discounted { get; set; } + + [DataMember(Name = "description", EmitDefaultValue = false)] + public string Description { get; set; } +} diff --git a/src/Model/CreateTransactionRequest/TaxOverride.cs b/src/Model/CreateTransactionRequest/TaxOverride.cs new file mode 100644 index 0000000..ae1e6f8 --- /dev/null +++ b/src/Model/CreateTransactionRequest/TaxOverride.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; + +[DataContract] +internal sealed class TaxOverride +{ + [DataMember(Name = "type", EmitDefaultValue = false)] + public string Type { get; set; } + + [DataMember(Name = "taxAmount", EmitDefaultValue = false)] + public double? TaxAmount { get; set; } + + [DataMember(Name = "taxDate", EmitDefaultValue = false)] + public DateTime TaxDate { get; set; } + + [DataMember(Name = "reason", EmitDefaultValue = false)] + public string Reason { get; set; } +} diff --git a/src/Model/CreateTransactionResponse/AvaTaxMessage.cs b/src/Model/CreateTransactionResponse/AvaTaxMessage.cs new file mode 100644 index 0000000..5286f65 --- /dev/null +++ b/src/Model/CreateTransactionResponse/AvaTaxMessage.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; + +[DataContract] +internal sealed class AvaTaxMessage +{ + [DataMember(Name = "summary")] + public string Summary { get; set; } + + [DataMember(Name = "details")] + public string Details { get; set; } + + [DataMember(Name = "refersTo")] + public string RefersTo { get; set; } + + [DataMember(Name = "severity")] + public string Severity { get; set; } + + [DataMember(Name = "source")] + public string Source { get; set; } +} \ No newline at end of file diff --git a/src/Model/CreateTransactionResponse/CreateTransactionResponse.cs b/src/Model/CreateTransactionResponse/CreateTransactionResponse.cs new file mode 100644 index 0000000..edfcf97 --- /dev/null +++ b/src/Model/CreateTransactionResponse/CreateTransactionResponse.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; + +[DataContract] +internal sealed class CreateTransactionResponse +{ + [DataMember(Name = "id")] + public long? Id { get; set; } + + [DataMember(Name = "code")] + public string Code { get; set; } + + [DataMember(Name = "date")] + public string Date { get; set; } + + [DataMember(Name = "paymentDate")] + public string PaymentDate { get; set; } + + [DataMember(Name = "status")] + public string Status { get; set; } + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "batchCode")] + public string BatchCode { get; set; } + + [DataMember(Name = "currencyCode")] + public string CurrencyCode { get; set; } + + [DataMember(Name = "exchangeRateCurrencyCode")] + public string ExchangeRateCurrencyCode { get; set; } + + [DataMember(Name = "customerUsageType")] + public string CustomerUsageType { get; set; } + + [DataMember(Name = "entityUseCode")] + public string EntityUseCode { get; set; } + + [DataMember(Name = "customerVendorCode")] + public string CustomerVendorCode { get; set; } + + [DataMember(Name = "customerCode")] + public string CustomerCode { get; set; } + + [DataMember(Name = "exemptNo")] + public string ExemptNo { get; set; } + + [DataMember(Name = "reconciled")] + public bool? Reconciled { get; set; } + + [DataMember(Name = "locationCode")] + public string LocationCode { get; set; } + + [DataMember(Name = "reportingLocationCode")] + public string ReportingLocationCode { get; set; } + + [DataMember(Name = "purchaseOrderNo")] + public string PurchaseOrderNo { get; set; } + + [DataMember(Name = "referenceCode")] + public string ReferenceCode { get; set; } + + [DataMember(Name = "salespersonCode")] + public string SalespersonCode { get; set; } + + [DataMember(Name = "taxOverrideType")] + public string TaxOverrideType { get; set; } + + [DataMember(Name = "taxOverrideAmount")] + public double? TaxOverrideAmount { get; set; } + + [DataMember(Name = "taxOverrideReason")] + public string TaxOverrideReason { get; set; } + + [DataMember(Name = "totalAmount")] + public double? TotalAmount { get; set; } + + [DataMember(Name = "totalExempt")] + public double? TotalExempt { get; set; } + + [DataMember(Name = "totalDiscount")] + public double? TotalDiscount { get; set; } + + [DataMember(Name = "totalTax")] + public double? TotalTax { get; set; } + + [DataMember(Name = "totalTaxable")] + public double? TotalTaxable { get; set; } + + [DataMember(Name = "totalTaxCalculated")] + public double? TotalTaxCalculated { get; set; } + + [DataMember(Name = "adjustmentReason")] + public string AdjustmentReason { get; set; } + + [DataMember(Name = "adjustmentDescription")] + public string AdjustmentDescription { get; set; } + + [DataMember(Name = "locked")] + public bool? Locked { get; set; } + + [DataMember(Name = "region")] + public string Region { get; set; } + + [DataMember(Name = "country")] + public string Country { get; set; } + + [DataMember(Name = "exchangeRateEffectiveDate")] + public string ExchangeRateEffectiveDate { get; set; } + + [DataMember(Name = "exchangeRate")] + public double? ExchangeRate { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "email")] + public string Email { get; set; } + + [DataMember(Name = "lines")] + public IEnumerable Lines { get; set; } + + [DataMember(Name = "addresses")] + public IEnumerable Addresses { get; set; } + + [DataMember(Name = "parameters")] + public IEnumerable Parameters { get; set; } + + [DataMember(Name = "messages")] + public IEnumerable Messages { get; set; } +} diff --git a/src/Model/CreateTransactionResponse/TransactionAddress.cs b/src/Model/CreateTransactionResponse/TransactionAddress.cs new file mode 100644 index 0000000..3e8f7e9 --- /dev/null +++ b/src/Model/CreateTransactionResponse/TransactionAddress.cs @@ -0,0 +1,46 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; + +[DataContract] +internal sealed class TransactionAddress +{ + [DataMember(Name = "id")] + public long? Id { get; set; } + + [DataMember(Name = "transactionId")] + public long? TransactionId { get; set; } + + [DataMember(Name = "boundaryLevel")] + public string BoundaryLevel { get; set; } + + [DataMember(Name = "line1")] + public string Line1 { get; set; } + + [DataMember(Name = "line2")] + public string Line2 { get; set; } + + [DataMember(Name = "line3")] + public string Line3 { get; set; } + + [DataMember(Name = "city")] + public string City { get; set; } + + [DataMember(Name = "region")] + public string Region { get; set; } + + [DataMember(Name = "postalCode")] + public string PostalCode { get; set; } + + [DataMember(Name = "country")] + public string Country { get; set; } + + [DataMember(Name = "taxRegionId")] + public int? TaxRegionId { get; set; } + + [DataMember(Name = "latitude")] + public string Latitude { get; set; } + + [DataMember(Name = "longitude")] + public string Longitude { get; set; } +} diff --git a/src/Model/CreateTransactionResponse/TransactionLine.cs b/src/Model/CreateTransactionResponse/TransactionLine.cs new file mode 100644 index 0000000..6acae28 --- /dev/null +++ b/src/Model/CreateTransactionResponse/TransactionLine.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; + +[DataContract] +internal sealed class TransactionLine +{ + [DataMember(Name = "id")] + public long? Id { get; set; } + + [DataMember(Name = "transactionId")] + public long? TransactionId { get; set; } + + [DataMember(Name = "lineNumber")] + public string LineNumber { get; set; } + + [DataMember(Name = "details")] + public IEnumerable Details { get; set; } +} diff --git a/src/Model/CreateTransactionResponse/TransactionLineDetail.cs b/src/Model/CreateTransactionResponse/TransactionLineDetail.cs new file mode 100644 index 0000000..7801ac3 --- /dev/null +++ b/src/Model/CreateTransactionResponse/TransactionLineDetail.cs @@ -0,0 +1,28 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; + +[DataContract] +internal sealed class TransactionLineDetail +{ + [DataMember(Name = "id")] + public long? Id { get; set; } + + [DataMember(Name = "transactionLineId")] + public long? TransactionLineId { get; set; } + + [DataMember(Name = "transactionId")] + public long? TransactionId { get; set; } + + [DataMember(Name = "tax")] + public double? Tax { get; set; } + + [DataMember(Name = "taxCalculated")] + public double? TaxCalculated { get; set; } + + [DataMember(Name = "taxName")] + public string TaxName { get; set; } + + [DataMember(Name = "jurisName")] + public string JurisName { get; set; } +} diff --git a/src/Model/CreateTransactionResponse/TransactionParameter.cs b/src/Model/CreateTransactionResponse/TransactionParameter.cs new file mode 100644 index 0000000..760adac --- /dev/null +++ b/src/Model/CreateTransactionResponse/TransactionParameter.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; + +[DataContract] +internal sealed class TransactionParameter +{ + [DataMember(Name = "name", EmitDefaultValue = false)] + public string Name { get; set; } + + [DataMember(Name = "value", EmitDefaultValue = false)] + public string Value { get; set; } + + [DataMember(Name = "unit", EmitDefaultValue = false)] + public string Unit { get; set; } +} diff --git a/src/Model/Enums/DocumentType.cs b/src/Model/Enums/DocumentType.cs new file mode 100644 index 0000000..968a649 --- /dev/null +++ b/src/Model/Enums/DocumentType.cs @@ -0,0 +1,8 @@ +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.Enums; + +internal enum DocumentType +{ + SalesOrder, + SalesInvoice, + ReturnInvoice +} diff --git a/src/Model/ResolveAddressResponse/ResolveAddressResponse.cs b/src/Model/ResolveAddressResponse/ResolveAddressResponse.cs new file mode 100644 index 0000000..9b47d4c --- /dev/null +++ b/src/Model/ResolveAddressResponse/ResolveAddressResponse.cs @@ -0,0 +1,19 @@ +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.ResolveAddressResponse; + +[DataContract] +internal sealed class ResolveAddressResponse +{ + [DataMember(Name = "address")] + public AddressLocationInfo Address { get; set; } + + [DataMember(Name = "validatedAddresses")] + public IEnumerable ValidatedAddresses { get; set; } + + [DataMember(Name = "messages")] + public IEnumerable Messages { get; set; } +} \ No newline at end of file diff --git a/src/Model/ResolveAddressResponse/ValidatedAddressInfo.cs b/src/Model/ResolveAddressResponse/ValidatedAddressInfo.cs new file mode 100644 index 0000000..9d1b83c --- /dev/null +++ b/src/Model/ResolveAddressResponse/ValidatedAddressInfo.cs @@ -0,0 +1,37 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.ResolveAddressResponse; + +[DataContract] +internal sealed class ValidatedAddressInfo +{ + [DataMember(Name = "addressType", EmitDefaultValue = false)] + public string AddressType { get; set; } + + [DataMember(Name = "line1", EmitDefaultValue = false)] + public string Line1 { get; set; } + + [DataMember(Name = "line2", EmitDefaultValue = false)] + public string Line2 { get; set; } + + [DataMember(Name = "line3", EmitDefaultValue = false)] + public string Line3 { get; set; } + + [DataMember(Name = "city", EmitDefaultValue = false)] + public string City { get; set; } + + [DataMember(Name = "region", EmitDefaultValue = false)] + public string Region { get; set; } + + [DataMember(Name = "country", EmitDefaultValue = false)] + public string Country { get; set; } + + [DataMember(Name = "postalCode", EmitDefaultValue = false)] + public string PostalCode { get; set; } + + [DataMember(Name = "latitude", EmitDefaultValue = false)] + public double? Latitude { get; set; } + + [DataMember(Name = "longitude", EmitDefaultValue = false)] + public double? Longitude { get; set; } +} \ No newline at end of file diff --git a/src/Model/Summary.cs b/src/Model/Summary.cs deleted file mode 100644 index 85ee749..0000000 --- a/src/Model/Summary.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model -{ - internal class Summary - { - public string country { get; set; } - public string region { get; set; } - public string jurisType { get; set; } - public string jurisCode { get; set; } - public string jurisName { get; set; } - public int taxAuthorityType { get; set; } - public string stateAssignedNo { get; set; } - public string taxType { get; set; } - public string taxSubType { get; set; } - public string taxName { get; set; } - public string rateType { get; set; } - public double taxable { get; set; } - public double rate { get; set; } - public double tax { get; set; } - public double taxCalculated { get; set; } - public double nonTaxable { get; set; } - public double exemption { get; set; } - } -} diff --git a/src/Model/VoidTransaction/Address.cs b/src/Model/VoidTransaction/Address.cs new file mode 100644 index 0000000..5c15e87 --- /dev/null +++ b/src/Model/VoidTransaction/Address.cs @@ -0,0 +1,46 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.VoidTransaction; + +[DataContract] +internal sealed class Address +{ + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "transactionId")] + public string TransactionId { get; set; } + + [DataMember(Name = "boundaryLevel")] + public string BoundaryLevel { get; set; } + + [DataMember(Name = "line1")] + public string Line1 { get; set; } + + [DataMember(Name = "line2")] + public string Line2 { get; set; } + + [DataMember(Name = "line3")] + public string Line3 { get; set; } + + [DataMember(Name = "city")] + public string City { get; set; } + + [DataMember(Name = "region")] + public string Region { get; set; } + + [DataMember(Name = "postalCode")] + public string PostalCode { get; set; } + + [DataMember(Name = "country")] + public string Country { get; set; } + + [DataMember(Name = "taxRegionId")] + public int TaxRegionId { get; set; } + + [DataMember(Name = "latitude")] + public string Latitude { get; set; } + + [DataMember(Name = "longitude")] + public string Longitude { get; set; } +} diff --git a/src/Model/VoidTransaction/Location.cs b/src/Model/VoidTransaction/Location.cs new file mode 100644 index 0000000..7c0cefb --- /dev/null +++ b/src/Model/VoidTransaction/Location.cs @@ -0,0 +1,76 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.VoidTransaction; + +[DataContract] +internal sealed class Location +{ + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "companyId")] + public int? CompanyId { get; set; } + + [DataMember(Name = "locationCode")] + public string LocationCode { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "isMarketplaceOutsideUsa")] + public bool? IsMarketplaceOutsideUsa { get; set; } + + [DataMember(Name = "line1")] + public string Line1 { get; set; } + + [DataMember(Name = "line2")] + public string Line2 { get; set; } + + [DataMember(Name = "line3")] + public string Line3 { get; set; } + + [DataMember(Name = "city")] + public string City { get; set; } + + [DataMember(Name = "county")] + public string County { get; set; } + + [DataMember(Name = "region")] + public string Region { get; set; } + + [DataMember(Name = "postalCode")] + public string PostalCode { get; set; } + + [DataMember(Name = "country")] + public string Country { get; set; } + + [DataMember(Name = "isDefault")] + public bool IsDefault { get; set; } + + [DataMember(Name = "isRegistered")] + public bool IsRegistered { get; set; } + + [DataMember(Name = "dbaName")] + public string DbaName { get; set; } + + [DataMember(Name = "outletName")] + public string OutletName { get; set; } + + [DataMember(Name = "effectiveDate")] + public string EffectiveDate { get; set; } + + [DataMember(Name = "endDate")] + public string EndDate { get; set; } + + [DataMember(Name = "lastTransactionDate")] + public string LastTransactionDate { get; set; } + + [DataMember(Name = "registeredDate")] + public string RegisteredDate { get; set; } + + [DataMember(Name = "createdDate")] + public string CreatedDate { get; set; } + + [DataMember(Name = "modifiedDate")] + public string ModifiedDate { get; set; } +} \ No newline at end of file diff --git a/src/Model/VoidTransaction/Message.cs b/src/Model/VoidTransaction/Message.cs new file mode 100644 index 0000000..aaacf9f --- /dev/null +++ b/src/Model/VoidTransaction/Message.cs @@ -0,0 +1,28 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.VoidTransaction; + +[DataContract] +internal sealed class Message +{ + [DataMember(Name = "details")] + public string Details { get; set; } + + [DataMember(Name = "helpLink")] + public string HelpLink { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "refersTo")] + public string RefersTo { get; set; } + + [DataMember(Name = "severity")] + public string Severity { get; set; } + + [DataMember(Name = "source")] + public string Source { get; set; } + + [DataMember(Name = "summary")] + public string Summary { get; set; } +} diff --git a/src/Model/VoidTransaction/VoidTransactionRequest.cs b/src/Model/VoidTransaction/VoidTransactionRequest.cs new file mode 100644 index 0000000..35aaa00 --- /dev/null +++ b/src/Model/VoidTransaction/VoidTransactionRequest.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.VoidTransaction; + +[DataContract] +internal sealed class VoidTransactionRequest +{ + [DataMember(Name = "code")] + public string Code { get; set; } +} diff --git a/src/Model/VoidTransaction/VoidTransactionResponse.cs b/src/Model/VoidTransaction/VoidTransactionResponse.cs new file mode 100644 index 0000000..d9c8c45 --- /dev/null +++ b/src/Model/VoidTransaction/VoidTransactionResponse.cs @@ -0,0 +1,159 @@ +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.VoidTransaction; + +[DataContract] +internal sealed class VoidTransactionResponse +{ + [DataMember(Name = "id")] + public long Id { get; set; } + + [DataMember(Name = "code")] + public string Code { get; set; } + + [DataMember(Name = "companyId")] + public int CompanyId { get; set; } + + [DataMember(Name = "date")] + public string Date { get; set; } + + [DataMember(Name = "paymentDate")] + public string PaymentDate { get; set; } + + [DataMember(Name = "status")] + public string Status { get; set; } + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "batchCode")] + public string BatchCode { get; set; } + + [DataMember(Name = "currencyCode")] + public string CurrencyCode { get; set; } + + [DataMember(Name = "exchangeRateCurrencyCode")] + public string ExchangeRateCurrencyCode { get; set; } + + [DataMember(Name = "customerUsageType")] + public string CustomerUsageType { get; set; } + + [DataMember(Name = "entityUseCode")] + public string EntityUseCode { get; set; } + + [DataMember(Name = "customerVendorCode")] + public string CustomerVendorCode { get; set; } + + [DataMember(Name = "customerCode")] + public string CustomerCode { get; set; } + + [DataMember(Name = "exemptNo")] + public string ExemptNo { get; set; } + + [DataMember(Name = "reconciled")] + public bool Reconciled { get; set; } + + [DataMember(Name = "locationCode")] + public string LocationCode { get; set; } + + [DataMember(Name = "reportingLocationCode")] + public string ReportingLocationCode { get; set; } + + [DataMember(Name = "purchaseOrderNo")] + public string PurchaseOrderNo { get; set; } + + [DataMember(Name = "referenceCode")] + public string ReferenceCode { get; set; } + + [DataMember(Name = "salespersonCode")] + public string SalespersonCode { get; set; } + + [DataMember(Name = "taxOverrideType")] + public string TaxOverrideType { get; set; } + + [DataMember(Name = "taxOverrideAmount")] + public double TaxOverrideAmount { get; set; } + + [DataMember(Name = "taxOverrideReason")] + public string TaxOverrideReason { get; set; } + + [DataMember(Name = "totalAmount")] + public double TotalAmount { get; set; } + + [DataMember(Name = "totalExempt")] + public double TotalExempt { get; set; } + + [DataMember(Name = "totalDiscount")] + public double TotalDiscount { get; set; } + + [DataMember(Name = "totalTax")] + public double TotalTax { get; set; } + + [DataMember(Name = "totalTaxable")] + public double TotalTaxable { get; set; } + + [DataMember(Name = "totalTaxCalculated")] + public double TotalTaxCalculated { get; set; } + + [DataMember(Name = "adjustmentReason")] + public string AdjustmentReason { get; set; } + + [DataMember(Name = "adjustmentDescription")] + public string AdjustmentDescription { get; set; } + + [DataMember(Name = "locked")] + public bool Locked { get; set; } + + [DataMember(Name = "region")] + public string Region { get; set; } + + [DataMember(Name = "country")] + public string Country { get; set; } + + [DataMember(Name = "originAddressId")] + public long OriginAddressId { get; set; } + + [DataMember(Name = "destinationAddressId")] + public long DestinationAddressId { get; set; } + + [DataMember(Name = "exchangeRateEffectiveDate")] + public string ExchangeRateEffectiveDate { get; set; } + + [DataMember(Name = "exchangeRate")] + public double ExchangeRate { get; set; } + + [DataMember(Name = "isSellerImporterOfRecord")] + public bool IsSellerImporterOfRecord { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "email")] + public string Email { get; set; } + + [DataMember(Name = "businessIdentificationNo")] + public string BusinessIdentificationNo { get; set; } + + [DataMember(Name = "modifiedDate")] + public string ModifiedDate { get; set; } + + [DataMember(Name = "modifiedUserId")] + public int ModifiedUserId { get; set; } + + [DataMember(Name = "taxDate")] + public string TaxDate { get; set; } + + [DataMember(Name = "lines")] + public IEnumerable Lines { get; set; } + + [DataMember(Name = "addresses")] + public IEnumerable
Addresses { get; set; } + + [DataMember(Name = "locationTypes")] + public IEnumerable LocationTypes { get; set; } + + [DataMember(Name = "messages")] + public IEnumerable Messages { get; set; } +} diff --git a/src/Notifications/BeforeTaxCalculationArgs.cs b/src/Notifications/BeforeTaxCalculationArgs.cs new file mode 100644 index 0000000..13fe535 --- /dev/null +++ b/src/Notifications/BeforeTaxCalculationArgs.cs @@ -0,0 +1,12 @@ +using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Extensibility.Notifications; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Notifications; + +/// +/// Before tax calculation arguments +/// +public class BeforeTaxCalculationArgs : CancelableNotificationArgs +{ + public Order Order { get; set; } +} diff --git a/src/Notifications/BeforeTaxCommitArgs.cs b/src/Notifications/BeforeTaxCommitArgs.cs new file mode 100644 index 0000000..aeb9ee5 --- /dev/null +++ b/src/Notifications/BeforeTaxCommitArgs.cs @@ -0,0 +1,12 @@ +using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Extensibility.Notifications; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Notifications; + +/// +/// Before tax commit arguments +/// +public class BeforeTaxCommitArgs : CancelableNotificationArgs +{ + public Order Order { get; set; } +} diff --git a/src/Notifications/OnGetCustomerCodeArgs.cs b/src/Notifications/OnGetCustomerCodeArgs.cs new file mode 100644 index 0000000..727d64b --- /dev/null +++ b/src/Notifications/OnGetCustomerCodeArgs.cs @@ -0,0 +1,14 @@ +using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Extensibility.Notifications; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Notifications; + +/// +/// Args class to get the customer code to send to Avalara. +/// +public class OnGetCustomerCodeArgs : NotificationArgs +{ + public Order Order { get; set; } + + public string CustomerCode { get; set; } +} diff --git a/src/Service/ApiCommand.cs b/src/Service/ApiCommand.cs new file mode 100644 index 0000000..65f8cbc --- /dev/null +++ b/src/Service/ApiCommand.cs @@ -0,0 +1,25 @@ +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Service; + +internal enum ApiCommand +{ + /// + /// Records a new transaction in AvaTax. + /// See: https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Transactions/CreateTransaction/ + /// POST /transactions/create + /// + CreateTransaction, + + /// + /// Retrieve geolocation information for a specified US or Canadian address + /// See: https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Addresses/ResolveAddress/ + /// GET /addresses/resolve + /// + ResolveAddress, + + /// + /// Void a transaction. + /// See: https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Transactions/VoidTransaction/ + /// POST /companies/{operatorId}/transactions/{OperatorSecondId}/void + /// + VoidTransaction +} diff --git a/src/Service/AvalaraRequest.cs b/src/Service/AvalaraRequest.cs new file mode 100644 index 0000000..a737d62 --- /dev/null +++ b/src/Service/AvalaraRequest.cs @@ -0,0 +1,145 @@ +using Dynamicweb.Core; +using Dynamicweb.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Service; + +/// +/// Send request to Avalara and get related response. +/// +internal static class AvalaraRequest +{ + public static string SendRequest(string accountId, string licenseKey, string apiUrl, CommandConfiguration configuration) + { + using var messageHandler = GetMessageHandler(); + using var client = new HttpClient(messageHandler); + + client.BaseAddress = new Uri(apiUrl); + client.Timeout = new TimeSpan(0, 0, 0, 90); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + string authenticationParameter = Convert.ToBase64String(Encoding.Default.GetBytes($"{accountId}:{licenseKey}")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authenticationParameter); + + string apiCommand = GetCommandLink( + apiUrl, + configuration.CommandType, + configuration.OperatorId, + configuration.OperatorSecondId, + configuration.QueryStringParameters + ); + + Task requestTask = configuration.CommandType switch + { + //GET + ApiCommand.ResolveAddress => client.GetAsync(apiCommand), + //POST + ApiCommand.CreateTransaction or + ApiCommand.VoidTransaction => client.PostAsync(apiCommand, GetStringContent(configuration)), + _ => throw new NotImplementedException($"Unknown operation was used. The operation code: {configuration.CommandType}.") + }; + + try + { + using HttpResponseMessage response = requestTask.GetAwaiter().GetResult(); + + string responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + if (configuration.DebugLog) + { + var logText = new StringBuilder("Remote server response:"); + logText.AppendLine($"HttpStatusCode = {response.StatusCode}"); + logText.AppendLine($"HttpStatusDescription = {response.ReasonPhrase}"); + logText.AppendLine($"Response text: {responseText}"); + + Log(logText.ToString(), false, configuration.CommandType); + } + + if (!response.IsSuccessStatusCode) + { + string errorMessage = $"Unhandled exception. Operation failed: {response.ReasonPhrase}. Response text: ${responseText}"; + Log(errorMessage, false, configuration.CommandType); + + throw new Exception(errorMessage); + } + + return responseText; + } + catch (HttpRequestException requestException) + { + string errorMessage = $"An error occurred during Avalara request. Error code: {requestException.StatusCode}"; + Log(errorMessage, false, configuration.CommandType); + throw new Exception(errorMessage); + } + + HttpMessageHandler GetMessageHandler() => new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip + }; + } + + private static HttpContent GetStringContent(CommandConfiguration configuration) + { + string content = Converter.SerializeCompact(configuration.Data); + + if (configuration.DebugLog) + Log($"Request data: {content}", true, configuration.CommandType); + + return new StringContent(content, Encoding.UTF8, "application/json"); + } + + private static string GetCommandLink(string baseAddress, ApiCommand command, string operatorId, string operatorSecondId, Dictionary queryParameters) + { + return command switch + { + ApiCommand.CreateTransaction => GetCommandLink("transactions/create"), + ApiCommand.ResolveAddress => GetCommandLink("addresses/resolve", queryParameters), + ApiCommand.VoidTransaction => GetCommandLink($"companies/{operatorId}/transactions/{operatorSecondId}/void"), + _ => throw new NotImplementedException($"The api command is not supported. Command: {command}") + }; + + string GetCommandLink(string gateway, Dictionary queryParameters = null) + { + string link = $"{baseAddress}/{gateway}"; + + if (queryParameters?.Count is 0 or null) + return link; + + string parameters = string.Join("&", queryParameters.Select(parameter => $"{parameter.Key}={parameter.Value}")); + + return $"{link}?{parameters}"; + } + } + + private static void Log(string message, bool isRequest, ApiCommand commandType) + { + string type = isRequest ? "Request" : "Response"; + var errorMessage = new StringBuilder($"{type} for command: '{commandType}'."); + errorMessage.AppendLine(message); + + if (commandType is ApiCommand.ResolveAddress) + LogAddressValidator(message); + else + LogAvalara(message); + } + + private static void LogAvalara(string message) + { + string fullName = typeof(AvalaraTaxProvider).FullName; + LogManager.Current.GetLogger($"/eCom/TaxProvider/{fullName}").Info(message); + LogManager.System.GetLogger("Provider", fullName).Info(message); + } + + private static void LogAddressValidator(string message) + { + string name = typeof(AvalaraAddressValidatorProvider).FullName ?? "AddressValidationProvider"; + LogManager.Current.GetLogger(string.Format("/eCom/AddressValidatorProvider/{0}", name)).Info(message); + LogManager.System.GetLogger(LogCategory.Provider, name).Info(message); + } +} diff --git a/src/Service/AvalaraService.cs b/src/Service/AvalaraService.cs new file mode 100644 index 0000000..a6bfc8a --- /dev/null +++ b/src/Service/AvalaraService.cs @@ -0,0 +1,102 @@ +using Dynamicweb.Core; +using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionResponse; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.ResolveAddressResponse; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.VoidTransaction; +using System; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Service; + +internal sealed class AvalaraService +{ + public string AccountId { get; set; } + + public string LicenseKey { get; set; } + + public bool DebugLog { get; set; } + + public bool TestMode { get; set; } + + public CreateTransactionResponse CreateAdjustTransaction(Order order, AvalaraTaxProvider providerData) => CreateTransaction(order, providerData, TransactionType.Adjust); + + public CreateTransactionResponse CreateCalculateTransaction(Order order, AvalaraTaxProvider providerData) => CreateTransaction(order, providerData, TransactionType.Calculate); + + public CreateTransactionResponse CreateCommitTransaction(Order order, AvalaraTaxProvider providerData) => CreateTransaction(order, providerData, TransactionType.Commit); + + public CreateTransactionResponse CreateProductReturnsTransaction(Order order, AvalaraTaxProvider providerData, Order originalOrder) + { + AvalaraTaxProvider.VerifyCustomFields(); + + var transactionHelper = new PrepareTransactionHelper(order, providerData); + CreateTransactionRequest request = transactionHelper.PrepareProductReturnRequest(originalOrder); + + return SendTransactionRequest(request); + } + + public ResolveAddressResponse ResolveAddress(AddressLocationInfo address) + { + var configuration = new CommandConfiguration + { + CommandType = ApiCommand.ResolveAddress, + DebugLog = DebugLog, + QueryStringParameters = new(StringComparer.OrdinalIgnoreCase) + { + ["line1"] = address.Line1, + ["line2"] = address.Line2, + ["line3"] = address.Line3, + ["city"] = address.City, + ["region"] = address.Region, + ["postalCode"] = address.PostalCode, + ["country"] = address.Country, + ["textCase"] = "Mixed" + } + }; + + string response = AvalaraRequest.SendRequest(AccountId, LicenseKey, GetBaseAddress(), configuration); + + return Converter.Deserialize(response); + } + + public VoidTransactionResponse VoidTransaction(string companyCode, string taxTransactionNumber) + { + var configuration = new CommandConfiguration + { + CommandType = ApiCommand.VoidTransaction, + DebugLog = DebugLog, + OperatorId = companyCode, + OperatorSecondId = taxTransactionNumber, + Data = new VoidTransactionRequest { Code = "DocVoided" } + }; + + string response = AvalaraRequest.SendRequest(AccountId, LicenseKey, GetBaseAddress(), configuration); + + return Converter.Deserialize(response); + } + + private CreateTransactionResponse CreateTransaction(Order order, AvalaraTaxProvider providerData, TransactionType transactionType) + { + AvalaraTaxProvider.VerifyCustomFields(); + + var transactionHelper = new PrepareTransactionHelper(order, providerData); + CreateTransactionRequest request = transactionHelper.PrepareTransactionRequest(transactionType); + + return SendTransactionRequest(request); + } + + private CreateTransactionResponse SendTransactionRequest(CreateTransactionRequest request) + { + string response = AvalaraRequest.SendRequest(AccountId, LicenseKey, GetBaseAddress(), new() + { + CommandType = ApiCommand.CreateTransaction, + DebugLog = DebugLog, + Data = request + }); + + return Converter.Deserialize(response); + } + + private string GetBaseAddress() => TestMode + ? "https://sandbox-rest.avatax.com/api/v2" + : "https://rest.avatax.com/api/v2"; +} diff --git a/src/Service/CommandConfiguration.cs b/src/Service/CommandConfiguration.cs new file mode 100644 index 0000000..06f271d --- /dev/null +++ b/src/Service/CommandConfiguration.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Service; + +internal sealed class CommandConfiguration +{ + /// + /// Create a log of the request and response from Avalara + /// + public bool DebugLog { get; set; } + + /// + /// Avalara command. See operation urls in and + /// + public ApiCommand CommandType { get; set; } + + /// + /// Command operator id, like https://.../{OperatorId} + /// + public string OperatorId { get; set; } + + /// + /// Command operator id, like https://.../{OperatorId}/.../{OperatorSecondId} + /// + public string OperatorSecondId { get; set; } + + /// + /// Data to serialize + /// + public object Data { get; set; } + + /// + /// Query string parameters for GET request + /// + public Dictionary QueryStringParameters { get; set; } +} diff --git a/src/Service/PrepareTransactionHelper.cs b/src/Service/PrepareTransactionHelper.cs new file mode 100644 index 0000000..6036142 --- /dev/null +++ b/src/Service/PrepareTransactionHelper.cs @@ -0,0 +1,257 @@ +using Dynamicweb.Core; +using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Ecommerce.Prices; +using Dynamicweb.Ecommerce.Products; +using Dynamicweb.Ecommerce.Products.Taxes; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.CreateTransactionRequest; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Model.Enums; +using Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Notifications; +using Dynamicweb.Extensibility.Notifications; +using Dynamicweb.Security.UserManagement; +using Dynamicweb.Security.UserManagement.Common.SystemFields; +using System; + +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider.Service; + +internal sealed class PrepareTransactionHelper +{ + public Order Order { get; } + public AvalaraTaxProvider Provider { get; } + + public PrepareTransactionHelper(Order order, AvalaraTaxProvider provider) + { + Order = order ?? throw new ArgumentNullException(nameof(order)); + Provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + public CreateTransactionRequest PrepareTransactionRequest(TransactionType transactionType) + { + if (transactionType is TransactionType.ProductReturns) + throw new InvalidOperationException($"Use {nameof(PrepareProductReturnRequest)} for TransactionType.ProductReturns."); + + var request = InitializeBaseRequest(transactionType); + + if (transactionType is TransactionType.Commit) + { + request.Commit = true; + request.Date = Order.Date; + } + else if (transactionType is TransactionType.Adjust) + SetAdjustData(request, Provider.EnableCommit); + + PopulateCommonDataAndLines(request); + + return request; + } + + public CreateTransactionRequest PrepareProductReturnRequest(Order originalOrder) + { + if (originalOrder is null) + throw new ArgumentNullException(nameof(originalOrder), "Original Order must be set for Product Returns tax request"); + + var request = InitializeBaseRequest(TransactionType.ProductReturns); + + SetReturnData(request, originalOrder, Provider.EnableCommit); + PopulateCommonDataAndLines(request); + + return request; + } + + private static DocumentType GetDocumentType(TransactionType transactionType) => transactionType switch + { + TransactionType.Adjust or TransactionType.Commit => DocumentType.SalesInvoice, + TransactionType.Calculate => DocumentType.SalesOrder, + TransactionType.ProductReturns => DocumentType.ReturnInvoice, + _ => throw new NotImplementedException($"Unknown or unsupported transaction type: {transactionType}") + }; + + private CreateTransactionRequest InitializeBaseRequest(TransactionType transactionType) + { + var request = new CreateTransactionRequest + { + CompanyCode = Provider.CompanyCode, + CustomerCode = GetCustomerCode(), + Date = DateTime.Now, + Type = GetDocumentType(transactionType).ToString(), + ReferenceCode = Order.Id, + CurrencyCode = Order.CurrencyCode + }; + + SetAddress(request); + SetCustomerExemptionData(request); + + return request; + } + + private void PopulateCommonDataAndLines(CreateTransactionRequest request) + { + var priceContext = new PriceContext(Order.Currency, Order.VatCountry); + int index = 0; + double orderDiscount = 0; + + foreach (OrderLine orderLine in Order.OrderLines) + { + if (Provider.IsTaxableTypeInternal(orderLine) || orderLine.HasType(OrderLineType.PointProduct)) + { + if (orderLine.Product is null) + continue; + + LineItem line = GetTaxLine(orderLine, index++, request.Addresses.ShipFrom, request.Addresses.ShipTo); + request.Lines.Add(line); + if (orderLine.HasType(OrderLineType.PointProduct)) + orderDiscount += -Convert.ToDouble(orderLine.Product.GetPrice(priceContext).PriceWithoutVAT); + } + else if (orderLine.HasType(OrderLineType.Discount) && string.IsNullOrEmpty(orderLine.GiftCardCode)) + orderDiscount += Convert.ToDouble(orderLine.Price.PriceWithoutVAT); + } + + orderDiscount = Math.Abs(orderDiscount); + if (orderDiscount > 0) + { + foreach (LineItem line in request.Lines) + { + if (string.Equals(line.TaxCode, Provider.TaxCodeShipping, StringComparison.Ordinal)) + line.Discounted = true; + } + request.Discount = orderDiscount; + } + + LineItem shippingLine = GetShippingTaxLine(request.Addresses.ShipFrom, request.Addresses.ShipTo); + if (shippingLine.Amount > 0) + request.Lines.Add(shippingLine); + } + + private void SetAdjustData(CreateTransactionRequest request, bool enableCommit) + { + request.TaxOverride = new() + { + Type = "TaxDate", + TaxAmount = 0, + Reason = "Adjust", + TaxDate = Order.Date + }; + request.Commit = !string.IsNullOrEmpty(Order.TaxTransactionNumber) && enableCommit; + } + + private void SetReturnData(CreateTransactionRequest request, Order originalOrder, bool enableCommit) + { + request.TaxOverride = new() + { + Type = "TaxDate", + TaxAmount = 0, + Reason = "Return", + TaxDate = originalOrder.Date + }; + + request.Date = Order.Date; + request.ReferenceCode = originalOrder.Id; + request.Commit = !string.IsNullOrEmpty(originalOrder.TaxTransactionNumber) && enableCommit; + } + + private void SetCustomerExemptionData(CreateTransactionRequest request) + { + if (Order.CustomerAccessUserId <= 0) + return; + + if (UserManagementServices.Users.GetUserById(Order.CustomerAccessUserId) is not User customer) + return; + + foreach (SystemFieldValue fieldValue in customer.SystemFieldValues) + { + if (string.Equals(fieldValue.SystemField.Name, AvalaraTaxProvider.ExemptionNumberFieldName, StringComparison.OrdinalIgnoreCase) && fieldValue.Value is not null) + request.ExemptionNumber = fieldValue.Value.ToString(); + else if (string.Equals(fieldValue.SystemField.Name, AvalaraTaxProvider.EntityUseCodeFieldName, StringComparison.OrdinalIgnoreCase) && fieldValue.Value is not null) + request.CustomerUsageType = fieldValue.Value.ToString(); + } + } + + private void SetAddress(CreateTransactionRequest request) + { + request.Addresses = new Addresses(); + request.Addresses.ShipFrom = AvalaraAddressValidatorProvider.GetOriginAddress(Provider); + + var destinationAddress = new AddressLocationInfo(); + destinationAddress = !string.IsNullOrEmpty(Order.DeliveryZip) + ? AvalaraAddressValidatorProvider.GetDeliveryAddress(Order) + : AvalaraAddressValidatorProvider.GetBillingAddress(Order); + + if (string.IsNullOrEmpty(destinationAddress.PostalCode)) + throw new InvalidOperationException("Make sure that the address is provided with a zip code."); + + request.Addresses.ShipTo = destinationAddress; + } + + private LineItem GetTaxLine(OrderLine orderLine, int index, AddressLocationInfo originAddress, AddressLocationInfo destinationAddress) + { + var line = new LineItem(); + var priceContext = new PriceContext(Order.Currency, Order.VatCountry); + + if (orderLine.Product is null) + throw new InvalidOperationException($"OrderLine {orderLine.Id} (Product: {orderLine.ProductName}) is missing associated Product data."); + + PriceInfo price = orderLine.HasType(OrderLineType.PointProduct) + ? orderLine.Product.GetPrice(priceContext) + : Provider.GetProductPriceWithoutDiscountsInternal(orderLine); + + line.Amount = Convert.ToDouble(price.PriceWithoutVAT); + line.Description = orderLine.ProductName; + line.Addresses = new() + { + ShipFrom = originAddress, + ShipTo = destinationAddress + }; + + line.Number = !string.IsNullOrEmpty(orderLine.Id) ? orderLine.Id : index.ToString(); + line.Quantity = Math.Abs((double)orderLine.Quantity); + + line.ItemCode = Services.Products.GetProductFieldValue(orderLine.Product, AvalaraTaxProvider.ItemCodeFieldName).ToString(); + line.TaxCode = Services.Products.GetProductFieldValue(orderLine.Product, AvalaraTaxProvider.TaxCodeFieldName).ToString(); + + return line; + } + + /// + /// "FR020100" - Avalara System TaxCode for SHIPPING + /// + private LineItem GetShippingTaxLine(AddressLocationInfo originAddress, AddressLocationInfo destinationAddress) + { + var line = new LineItem(); + line.Amount = Converter.ToDouble(Order.ShippingFee?.PriceWithoutVAT); + + line.Description = "SHIPPING"; + line.Addresses = new Addresses + { + ShipFrom = originAddress, + ShipTo = destinationAddress + }; + + line.Number = TaxProvider.ShippingCode; + line.TaxCode = Provider.TaxCodeShipping; + + return line; + } + + private string GetCustomerCode() + { + var notificationArgs = new OnGetCustomerCodeArgs { Order = Order }; + NotificationManager.Notify(AvalaraTaxProvider.OnGetCustomerCode, notificationArgs); + + if (!string.IsNullOrEmpty(notificationArgs.CustomerCode)) + return notificationArgs.CustomerCode; + + if (!string.IsNullOrEmpty(Provider.GetCustomerCodeFrom)) + { + return Provider.GetCustomerCodeFrom switch + { + nameof(CustomerCodeSource.OrderCustomerAccessUserId) => Order.CustomerAccessUserId.ToString(), + nameof(CustomerCodeSource.OrderCustomerNumber) => Order.CustomerNumber ?? "", + nameof(CustomerCodeSource.AccessUserExternalId) => Order.CustomerAccessUserId > 0 + ? UserManagementServices.Users.GetUserById(Order.CustomerAccessUserId)?.ExternalID ?? "" + : string.Empty, + _ => throw new NotImplementedException($"Unsupported option is used: {Provider.GetCustomerCodeFrom}") + }; + } + + return Order.CustomerAccessUserId.ToString(); + } +} diff --git a/src/TransactionType.cs b/src/TransactionType.cs new file mode 100644 index 0000000..f851b7a --- /dev/null +++ b/src/TransactionType.cs @@ -0,0 +1,9 @@ +namespace Dynamicweb.Ecommerce.TaxProviders.AvalaraTaxProvider; + +internal enum TransactionType +{ + Calculate, + Commit, + Adjust, + ProductReturns +}