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