From c53f7f487746a75aaafc5c65e145341ffd27edc9 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Tue, 13 May 2025 21:05:15 +1000 Subject: [PATCH 1/3] Removed external avalara library (because it's not our API package and we don't use external API packages in other places of our project) Refactored code --- src/Avalara-logo.png | Bin 0 -> 66417 bytes src/AvalaraAddressValidatorProvider.cs | 393 ++++---- src/AvalaraTaxProvider.cs | 896 +++++------------- src/CustomerCodeSource.cs | 8 + ...rce.TaxProviders.AvalaraTaxProvider.csproj | 21 +- src/Model/Address.cs | 19 - src/Model/CancelTransactionResponse.cs | 63 -- .../AddressLocationInfo.cs | 37 + .../CreateTransactionRequest/Addresses.cs | 13 + .../CreateTransactionRequest.cs | 49 + .../CreateTransactionRequest/LineItem.cs | 31 + .../CreateTransactionRequest/TaxOverride.cs | 20 + .../AvaTaxMessage.cs | 22 + .../CreateTransactionResponse.cs | 134 +++ .../TransactionAddress.cs | 46 + .../TransactionLine.cs | 20 + .../TransactionLineDetail.cs | 28 + .../TransactionParameter.cs | 16 + src/Model/Enums/DocumentType.cs | 8 + .../ResolveAddressResponse.cs | 19 + .../ValidatedAddressInfo.cs | 37 + src/Model/Summary.cs | 23 - src/Model/VoidTransaction/Address.cs | 46 + src/Model/VoidTransaction/Location.cs | 76 ++ src/Model/VoidTransaction/Message.cs | 28 + .../VoidTransaction/VoidTransactionRequest.cs | 10 + .../VoidTransactionResponse.cs | 159 ++++ src/Notifications/BeforeTaxCalculationArgs.cs | 12 + src/Notifications/BeforeTaxCommitArgs.cs | 12 + src/Notifications/OnGetCustomerCodeArgs.cs | 14 + src/Service/ApiCommand.cs | 25 + src/Service/AvalaraRequest.cs | 123 +++ src/Service/AvalaraService.cs | 102 ++ src/Service/CommandConfiguration.cs | 36 + src/Service/PrepareTransactionHelper.cs | 257 +++++ src/TransactionType.cs | 9 + 36 files changed, 1818 insertions(+), 994 deletions(-) create mode 100644 src/Avalara-logo.png create mode 100644 src/CustomerCodeSource.cs delete mode 100644 src/Model/Address.cs delete mode 100644 src/Model/CancelTransactionResponse.cs create mode 100644 src/Model/CreateTransactionRequest/AddressLocationInfo.cs create mode 100644 src/Model/CreateTransactionRequest/Addresses.cs create mode 100644 src/Model/CreateTransactionRequest/CreateTransactionRequest.cs create mode 100644 src/Model/CreateTransactionRequest/LineItem.cs create mode 100644 src/Model/CreateTransactionRequest/TaxOverride.cs create mode 100644 src/Model/CreateTransactionResponse/AvaTaxMessage.cs create mode 100644 src/Model/CreateTransactionResponse/CreateTransactionResponse.cs create mode 100644 src/Model/CreateTransactionResponse/TransactionAddress.cs create mode 100644 src/Model/CreateTransactionResponse/TransactionLine.cs create mode 100644 src/Model/CreateTransactionResponse/TransactionLineDetail.cs create mode 100644 src/Model/CreateTransactionResponse/TransactionParameter.cs create mode 100644 src/Model/Enums/DocumentType.cs create mode 100644 src/Model/ResolveAddressResponse/ResolveAddressResponse.cs create mode 100644 src/Model/ResolveAddressResponse/ValidatedAddressInfo.cs delete mode 100644 src/Model/Summary.cs create mode 100644 src/Model/VoidTransaction/Address.cs create mode 100644 src/Model/VoidTransaction/Location.cs create mode 100644 src/Model/VoidTransaction/Message.cs create mode 100644 src/Model/VoidTransaction/VoidTransactionRequest.cs create mode 100644 src/Model/VoidTransaction/VoidTransactionResponse.cs create mode 100644 src/Notifications/BeforeTaxCalculationArgs.cs create mode 100644 src/Notifications/BeforeTaxCommitArgs.cs create mode 100644 src/Notifications/OnGetCustomerCodeArgs.cs create mode 100644 src/Service/ApiCommand.cs create mode 100644 src/Service/AvalaraRequest.cs create mode 100644 src/Service/AvalaraService.cs create mode 100644 src/Service/CommandConfiguration.cs create mode 100644 src/Service/PrepareTransactionHelper.cs create mode 100644 src/TransactionType.cs diff --git a/src/Avalara-logo.png b/src/Avalara-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2341e294c6933df8c946987dba03abbb551e056f GIT binary patch literal 66417 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%0D&$iGx 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

cRdiZD`KzhL8P{G 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 literal 0 HcmV?d00001 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..9c4c208 --- /dev/null +++ b/src/Service/AvalaraRequest.cs @@ -0,0 +1,123 @@ +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, GetContent()), + _ => throw new NotSupportedException($"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}"); + + if (configuration.CommandType is ApiCommand.ResolveAddress) + LogAddressValidator(logText.ToString()); + else + Log(logText.ToString()); + } + + if (!response.IsSuccessStatusCode) + throw new Exception($"Unhandled exception. Operation failed: {response.ReasonPhrase}. Response text: ${responseText}"); + + return responseText; + } + } + catch (HttpRequestException requestException) + { + throw new Exception($"An error occurred during Avalara request. Error code: {requestException.StatusCode}"); + } + } + } + + HttpMessageHandler GetMessageHandler() => new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip + }; + + HttpContent GetContent() + { + string content = Converter.SerializeCompact(configuration.Data); + + 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 NotSupportedException($"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) + { + 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..43b8306 --- /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 ArgumentOutOfRangeException(nameof(transactionType), $"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 not null) + { + 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) + { + 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 Exception($"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 +} From fbe175e68f5264bf2b4f7dc6037a958a4ab11326 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Tue, 20 May 2025 00:23:00 +1000 Subject: [PATCH 2/3] Code review changes. --- src/Service/AvalaraRequest.cs | 132 ++++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 55 deletions(-) diff --git a/src/Service/AvalaraRequest.cs b/src/Service/AvalaraRequest.cs index 9c4c208..a737d62 100644 --- a/src/Service/AvalaraRequest.cs +++ b/src/Service/AvalaraRequest.cs @@ -18,70 +18,80 @@ internal static class AvalaraRequest { public static string SendRequest(string accountId, string licenseKey, string apiUrl, CommandConfiguration configuration) { - using (var messageHandler = GetMessageHandler()) + 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 (var client = new HttpClient(messageHandler)) + 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) { - 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, GetContent()), - _ => throw new NotSupportedException($"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}"); - - if (configuration.CommandType is ApiCommand.ResolveAddress) - LogAddressValidator(logText.ToString()); - else - Log(logText.ToString()); - } - - if (!response.IsSuccessStatusCode) - throw new Exception($"Unhandled exception. Operation failed: {response.ReasonPhrase}. Response text: ${responseText}"); - - return responseText; - } - } - catch (HttpRequestException requestException) - { - throw new Exception($"An error occurred during Avalara request. Error code: {requestException.StatusCode}"); - } + 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 }; + } - HttpContent GetContent() - { - string content = Converter.SerializeCompact(configuration.Data); + private static HttpContent GetStringContent(CommandConfiguration configuration) + { + string content = Converter.SerializeCompact(configuration.Data); - return new StringContent(content, Encoding.UTF8, "application/json"); - } + 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) @@ -91,7 +101,7 @@ private static string GetCommandLink(string baseAddress, ApiCommand command, str ApiCommand.CreateTransaction => GetCommandLink("transactions/create"), ApiCommand.ResolveAddress => GetCommandLink("addresses/resolve", queryParameters), ApiCommand.VoidTransaction => GetCommandLink($"companies/{operatorId}/transactions/{operatorSecondId}/void"), - _ => throw new NotSupportedException($"The api command is not supported. Command: {command}") + _ => throw new NotImplementedException($"The api command is not supported. Command: {command}") }; string GetCommandLink(string gateway, Dictionary queryParameters = null) @@ -107,7 +117,19 @@ string GetCommandLink(string gateway, Dictionary queryParameters } } - private static void Log(string message) + 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); From 2a138501f8ad24d98d1f6f79970e9e4b4321de18 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Tue, 20 May 2025 00:48:01 +1000 Subject: [PATCH 3/3] Additional changes. --- src/Service/PrepareTransactionHelper.cs | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Service/PrepareTransactionHelper.cs b/src/Service/PrepareTransactionHelper.cs index 43b8306..6036142 100644 --- a/src/Service/PrepareTransactionHelper.cs +++ b/src/Service/PrepareTransactionHelper.cs @@ -62,7 +62,7 @@ public CreateTransactionRequest PrepareProductReturnRequest(Order originalOrder) TransactionType.Adjust or TransactionType.Commit => DocumentType.SalesInvoice, TransactionType.Calculate => DocumentType.SalesOrder, TransactionType.ProductReturns => DocumentType.ReturnInvoice, - _ => throw new ArgumentOutOfRangeException(nameof(transactionType), $"Unknown or unsupported transaction type: {transactionType}") + _ => throw new NotImplementedException($"Unknown or unsupported transaction type: {transactionType}") }; private CreateTransactionRequest InitializeBaseRequest(TransactionType transactionType) @@ -93,13 +93,13 @@ private void PopulateCommonDataAndLines(CreateTransactionRequest request) { if (Provider.IsTaxableTypeInternal(orderLine) || orderLine.HasType(OrderLineType.PointProduct)) { - if (orderLine.Product is not null) - { - 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); - } + 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); @@ -150,18 +150,18 @@ private void SetReturnData(CreateTransactionRequest request, Order originalOrder private void SetCustomerExemptionData(CreateTransactionRequest request) { - if (Order.CustomerAccessUserId > 0) - { - if (UserManagementServices.Users.GetUserById(Order.CustomerAccessUserId) is not User customer) - return; + if (Order.CustomerAccessUserId <= 0) + 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(); - } + 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(); } } @@ -248,7 +248,7 @@ private string GetCustomerCode() nameof(CustomerCodeSource.AccessUserExternalId) => Order.CustomerAccessUserId > 0 ? UserManagementServices.Users.GetUserById(Order.CustomerAccessUserId)?.ExternalID ?? "" : string.Empty, - _ => throw new Exception($"Unsupported option is used: {Provider.GetCustomerCodeFrom}") + _ => throw new NotImplementedException($"Unsupported option is used: {Provider.GetCustomerCodeFrom}") }; }