Try your search with a different keyword or use * as a wildcard.
using System.Globalization;
using System.Text;
using Avalara.AvaTax.RestClient;
using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Tax;
using Nop.Core.Infrastructure;
using Nop.Data;
using Nop.Plugin.Tax.Avalara.Domain;
using Nop.Plugin.Tax.Avalara.ItemClassificationAPI;
using Nop.Services.Attributes;
using Nop.Services.Catalog;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Logging;
using Nop.Services.Orders;
using Nop.Services.Payments;
using Nop.Services.Tax;
namespace Nop.Plugin.Tax.Avalara.Services;
///
/// Represents the manager that operates with requests to the Avalara services
///
public class AvalaraTaxManager : IDisposable
{
#region Fields
protected readonly AvalaraTaxSettings _avalaraTaxSettings;
protected readonly IAddressService _addressService;
protected readonly IAttributeParser _checkoutAttributeParser;
protected readonly ICategoryService _categoryService;
protected readonly ICountryService _countryService;
protected readonly ICustomerService _customerService;
protected readonly IGenericAttributeService _genericAttributeService;
protected readonly IGeoLookupService _geoLookupService;
protected readonly ILogger _logger;
protected readonly INopFileProvider _fileProvider;
protected readonly IOrderService _orderService;
protected readonly IOrderTotalCalculationService _orderTotalCalculationService;
protected readonly IPaymentService _paymentService;
protected readonly IProductAttributeService _productAttributeService;
protected readonly IProductService _productService;
protected readonly IRepository _genericAttributeRepository;
protected readonly IRepository _taxCategoryRepository;
protected readonly IShoppingCartService _shoppingCartService;
protected readonly IStateProvinceService _stateProvinceService;
protected readonly IStaticCacheManager _staticCacheManager;
protected readonly ITaxCategoryService _taxCategoryService;
protected readonly ItemClassificationHttpClient _itemClassificationHttpClient;
protected readonly ItemClassificationService _itemClassificationService;
protected readonly IWorkContext _workContext;
protected readonly ShippingSettings _shippingSettings;
protected readonly TaxSettings _taxSettings;
protected readonly TaxTransactionLogService _taxTransactionLogService;
protected AvaTaxClient _serviceClient;
protected bool _disposed;
private static readonly char[] _separator = [','];
#endregion
#region Ctor
public AvalaraTaxManager(AvalaraTaxSettings avalaraTaxSettings,
IAddressService addressService,
IAttributeParser checkoutAttributeParser,
ICategoryService categoryService,
ICountryService countryService,
ICustomerService customerService,
IGenericAttributeService genericAttributeService,
IGeoLookupService geoLookupService,
ILogger logger,
INopFileProvider fileProvider,
IOrderService orderService,
IOrderTotalCalculationService orderTotalCalculationService,
IPaymentService paymentService,
IProductAttributeService productAttributeService,
IProductService productService,
IRepository genericAttributeRepository,
IRepository taxCategoryRepository,
IShoppingCartService shoppingCartService,
IStateProvinceService stateProvinceService,
IStaticCacheManager staticCacheManager,
ITaxCategoryService taxCategoryService,
ItemClassificationHttpClient itemClassificationHttpClient,
ItemClassificationService itemClassificationService,
IWorkContext workContext,
ShippingSettings shippingSettings,
TaxSettings taxSettings,
TaxTransactionLogService taxTransactionLogService)
{
_avalaraTaxSettings = avalaraTaxSettings;
_addressService = addressService;
_checkoutAttributeParser = checkoutAttributeParser;
_categoryService = categoryService;
_countryService = countryService;
_customerService = customerService;
_genericAttributeService = genericAttributeService;
_geoLookupService = geoLookupService;
_logger = logger;
_fileProvider = fileProvider;
_orderService = orderService;
_orderTotalCalculationService = orderTotalCalculationService;
_paymentService = paymentService;
_productAttributeService = productAttributeService;
_productService = productService;
_genericAttributeRepository = genericAttributeRepository;
_taxCategoryRepository = taxCategoryRepository;
_shoppingCartService = shoppingCartService;
_stateProvinceService = stateProvinceService;
_staticCacheManager = staticCacheManager;
_taxCategoryService = taxCategoryService;
_itemClassificationHttpClient = itemClassificationHttpClient;
_itemClassificationService = itemClassificationService;
_workContext = workContext;
_shippingSettings = shippingSettings;
_taxSettings = taxSettings;
_taxTransactionLogService = taxTransactionLogService;
}
#endregion
#region Utilities
#region Common
///
/// Event handler
///
/// Sender
/// Event args
protected async void OnCallCompleted(object sender, EventArgs args)
{
if (args is not AvaTaxCallEventArgs avaTaxCallEventArgs)
return;
var customer = await _workContext.GetCurrentCustomerAsync();
//log request results
await _taxTransactionLogService.InsertTaxTransactionLogAsync(new TaxTransactionLog
{
StatusCode = (int)avaTaxCallEventArgs.Code,
Url = avaTaxCallEventArgs.RequestUri.ToString(),
RequestMessage = avaTaxCallEventArgs.RequestBody,
ResponseMessage = avaTaxCallEventArgs.ResponseString,
CustomerId = customer.Id,
CreatedDateUtc = DateTime.UtcNow
});
}
///
/// Check that tax provider is configured
///
/// True if it's configured; otherwise false
protected bool IsConfigured()
{
return !string.IsNullOrEmpty(_avalaraTaxSettings.AccountId)
&& !string.IsNullOrEmpty(_avalaraTaxSettings.LicenseKey);
}
///
/// Handle function and get result with Error message in view
///
/// Result type
/// Function
///
/// A task that represents the asynchronous operation
/// The task result contains the result
///
protected async Task<(TResult Result, string Error)> HandleFunctionExAsync(Func> function)
{
try
{
//ensure that Avalara tax provider is configured
if (!IsConfigured())
throw new NopException("Tax provider is not configured");
return (await function(), default);
}
catch (Exception exception)
{
//compose an error message
var errorMessage = exception.Message;
if (exception is AvaTaxError avaTaxError && avaTaxError.error != null)
{
var errorInfo = avaTaxError.error.error;
if (errorInfo != null)
{
errorMessage = $"{errorInfo.code} - {errorInfo.message}{Environment.NewLine}";
if (errorInfo.details?.Any() ?? false)
{
var errorDetails = errorInfo.details.Aggregate(string.Empty, (error, detail) => $"{error}{detail.description}{Environment.NewLine}");
errorMessage = $"{errorMessage} Details: {errorDetails}";
}
}
}
//log errors
await _logger.ErrorAsync($"{AvalaraTaxDefaults.SystemName} error. {errorMessage}", exception, await _workContext.GetCurrentCustomerAsync());
return (default, errorMessage);
}
}
///
/// Handle function and get result
///
/// Result type
/// Function
///
/// A task that represents the asynchronous operation
/// The task result contains the result
///
protected async Task HandleFunctionAsync(Func> function)
{
try
{
//ensure that Avalara tax provider is configured
if (!IsConfigured())
throw new NopException("Tax provider is not configured");
return await function();
}
catch (Exception exception)
{
//compose an error message
var errorMessage = exception.Message;
if (exception is AvaTaxError avaTaxError && avaTaxError.error != null)
{
var errorInfo = avaTaxError.error.error;
if (errorInfo != null)
{
errorMessage = $"{errorInfo.code} - {errorInfo.message}{Environment.NewLine}";
if (errorInfo.details?.Any() ?? false)
{
var errorDetails = errorInfo.details.Aggregate(string.Empty, (error, detail) => $"{error}{detail.description}{Environment.NewLine}");
errorMessage = $"{errorMessage} Details: {errorDetails}";
}
}
}
//log errors
await _logger.ErrorAsync($"{AvalaraTaxDefaults.SystemName} error. {errorMessage}", exception, await _workContext.GetCurrentCustomerAsync());
return default;
}
}
#endregion
#region Tax calculation
///
/// Create tax transaction
///
/// Transaction details
/// Created transaction
protected TransactionModel CreateTransaction(CreateTransactionModel model)
{
var transaction = ServiceClient.CreateTransaction(null, model)
?? throw new NopException("No response from the service");
//whether there are any errors
if (transaction.messages?.Any() ?? false)
{
var message = transaction.messages.Aggregate(string.Empty, (error, message) => $"{error}{message.summary}{Environment.NewLine}");
throw new NopException(message);
}
return transaction;
}
///
/// Prepare model to create a tax transaction
///
/// Tax address
/// Customer code
/// Transaction document type
///
/// A task that represents the asynchronous operation
/// The task result contains the model
///
protected async Task PrepareTransactionModelAsync(Address address, string customerCode, DocumentType documentType)
{
var model = new CreateTransactionModel
{
customerCode = CommonHelper.EnsureMaximumLength(customerCode, 50),
date = DateTime.UtcNow,
type = documentType
};
//set company code
var companyCode = !string.IsNullOrEmpty(_avalaraTaxSettings.CompanyCode)
&& !_avalaraTaxSettings.CompanyCode.Equals(Guid.Empty.ToString())
? _avalaraTaxSettings.CompanyCode
: null;
model.companyCode = CommonHelper.EnsureMaximumLength(companyCode, 25);
//set tax addresses
model.addresses = new AddressesModel();
var originAddress = _avalaraTaxSettings.TaxOriginAddressType switch
{
TaxOriginAddressType.ShippingOrigin => await _addressService.GetAddressByIdAsync(_shippingSettings.ShippingOriginAddressId),
TaxOriginAddressType.DefaultTaxAddress => await _addressService.GetAddressByIdAsync(_taxSettings.DefaultTaxAddressId),
_ => null
};
var shipFromAddress = await MapAddressAsync(originAddress);
var shipToAddress = await MapAddressAsync(address);
if (shipFromAddress != null && shipToAddress != null)
{
model.addresses.shipFrom = shipFromAddress;
model.addresses.shipTo = shipToAddress;
}
else
model.addresses.singleLocation = shipToAddress ?? shipFromAddress;
return model;
}
///
/// Prepare order addresses
///
/// Customer
/// Order
/// Store id
/// A task that represents the asynchronous operation
protected async Task PrepareOrderAddressesAsync(Customer customer, Order order, int storeId)
{
order.BillingAddressId = customer.BillingAddressId ?? 0;
order.ShippingAddressId = customer.ShippingAddressId;
if (_shippingSettings.AllowPickupInStore)
{
var pickupPoint = await _genericAttributeService
.GetAttributeAsync(customer, NopCustomerDefaults.SelectedPickupPointAttribute, storeId);
if (pickupPoint != null)
{
var country = await _countryService.GetCountryByTwoLetterIsoCodeAsync(pickupPoint.CountryCode);
var state = await _stateProvinceService.GetStateProvinceByAbbreviationAsync(pickupPoint.StateAbbreviation, country?.Id);
var pickupAddress = new Address
{
Address1 = pickupPoint.Address,
City = pickupPoint.City,
CountryId = country?.Id,
StateProvinceId = state?.Id,
ZipPostalCode = pickupPoint.ZipPostalCode,
CreatedOnUtc = DateTime.UtcNow,
};
await _addressService.InsertAddressAsync(pickupAddress);
order.PickupAddressId = pickupAddress.Id;
}
}
}
///
/// Get a tax address of the passed order
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the address
///
protected async Task GetTaxAddressAsync(Order order)
{
Address address = null;
//tax is based on billing address
if (_taxSettings.TaxBasedOn == TaxBasedOn.BillingAddress &&
await _addressService.GetAddressByIdAsync(order.BillingAddressId) is Address billingAddress)
{
address = billingAddress;
}
//tax is based on shipping address
if (_taxSettings.TaxBasedOn == TaxBasedOn.ShippingAddress &&
order.ShippingAddressId.HasValue &&
await _addressService.GetAddressByIdAsync(order.ShippingAddressId.Value) is Address shippingAddress)
{
address = shippingAddress;
}
//tax is based on pickup point address
if (_taxSettings.TaxBasedOnPickupPointAddress &&
order.PickupAddressId.HasValue &&
await _addressService.GetAddressByIdAsync(order.PickupAddressId.Value) is Address pickupAddress)
{
address = pickupAddress;
}
//or use default address for tax calculation
address ??= await _addressService.GetAddressByIdAsync(_taxSettings.DefaultTaxAddressId);
return address;
}
///
/// Map address model
///
/// Address
///
/// A task that represents the asynchronous operation
/// The task result contains the address model
///
protected async Task MapAddressAsync(Address address)
{
return address == null ? null : new AddressLocationInfo
{
city = CommonHelper.EnsureMaximumLength(address.City, 50),
country = CommonHelper.EnsureMaximumLength((await _countryService.GetCountryByAddressAsync(address))?.TwoLetterIsoCode, 2),
line1 = CommonHelper.EnsureMaximumLength(address.Address1, 50),
line2 = CommonHelper.EnsureMaximumLength(address.Address2, 100),
postalCode = CommonHelper.EnsureMaximumLength(address.ZipPostalCode, 11),
region = CommonHelper.EnsureMaximumLength((await _stateProvinceService.GetStateProvinceByAddressAsync(address))?.Abbreviation, 3)
};
}
///
/// Get item lines to create tax transaction
///
/// Order
/// Order items
/// Destination country
///
/// A task that represents the asynchronous operation
/// The task result contains the list of item lines
///
protected async Task> GetItemLinesAsync(Order order, IList orderItems, int? countryId)
{
//get purchased products details
var items = await CreateLinesForOrderItemsAsync(order, orderItems, countryId);
//set payment method additional fee as the separate item line
if (order.PaymentMethodAdditionalFeeExclTax > decimal.Zero)
items.Add(await CreateLineForPaymentMethodAsync(order));
//set shipping rate as the separate item line
if (order.OrderShippingExclTax > decimal.Zero)
items.Add(await CreateLineForShippingAsync(order));
//set checkout attributes as the separate item lines
if (!string.IsNullOrEmpty(order.CheckoutAttributesXml))
items.AddRange(await CreateLinesForCheckoutAttributesAsync(order));
return items;
}
///
/// Create item lines for purchased order items
///
/// Order
/// Order items
/// Destination country
///
/// A task that represents the asynchronous operation
/// The task result contains the collection of item lines
///
protected async Task> CreateLinesForOrderItemsAsync(Order order, IList orderItems, int? countryId)
{
var itemsClassification = countryId > 0
? (await _itemClassificationService.GetItemClassificationAsync(countryId)).ToList()
: new();
return await orderItems.SelectAwait(async orderItem =>
{
var product = await _productService.GetProductByIdAsync(orderItem.ProductId);
var item = new LineItemModel
{
amount = orderItem.PriceExclTax,
//set name as item description to avoid long values
description = CommonHelper.EnsureMaximumLength(product?.Name, 2096),
//whether the discount to the item was applied
discounted = order.OrderSubTotalDiscountExclTax > decimal.Zero,
//product exemption
exemptionCode = product?.IsTaxExempt ?? false
? CommonHelper.EnsureMaximumLength($"Exempt-product-#{product.Id}", 25)
: string.Empty,
//set SKU as item code
itemCode = product != null
? CommonHelper.EnsureMaximumLength(await _productService.FormatSkuAsync(product, orderItem.AttributesXml), 50)
: string.Empty,
quantity = orderItem.Quantity
};
//set tax code
var productTaxCategory = await _taxCategoryService.GetTaxCategoryByIdAsync(product?.TaxCategoryId ?? 0);
item.taxCode = CommonHelper.EnsureMaximumLength(productTaxCategory?.Name, 25);
//whether entity use code is set
var entityUseCode = product != null
? await _genericAttributeService.GetAttributeAsync(product, AvalaraTaxDefaults.EntityUseCodeAttribute)
: string.Empty;
item.customerUsageType = CommonHelper.EnsureMaximumLength(entityUseCode, 25);
//set HS code
item.hsCode = itemsClassification.FirstOrDefault(x => x.ProductId == orderItem.ProductId)?.HSCode;
return item;
}).ToListAsync();
}
///
/// Create a separate item line for the order payment method additional fee
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the item line
///
protected async Task CreateLineForPaymentMethodAsync(Order order)
{
var paymentItem = new LineItemModel
{
amount = order.PaymentMethodAdditionalFeeExclTax,
//item description
description = "Payment method additional fee",
//set payment method system name as item code
itemCode = CommonHelper.EnsureMaximumLength(order.PaymentMethodSystemName, 50),
quantity = 1
};
//whether payment is taxable
if (_taxSettings.PaymentMethodAdditionalFeeIsTaxable)
{
//try to get tax code
var paymentTaxCategory = await _taxCategoryService.GetTaxCategoryByIdAsync(_taxSettings.PaymentMethodAdditionalFeeTaxClassId);
paymentItem.taxCode = CommonHelper.EnsureMaximumLength(paymentTaxCategory?.Name, 25);
}
else
{
//if payment is non-taxable, set it as exempt
paymentItem.exemptionCode = "Payment-fee-non-taxable";
}
return paymentItem;
}
///
/// Create a separate item line for the order shipping charge
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the item line
///
protected async Task CreateLineForShippingAsync(Order order)
{
var shippingItem = new LineItemModel
{
amount = order.OrderShippingExclTax,
//item description
description = "Shipping rate",
//set shipping method name as item code
itemCode = CommonHelper.EnsureMaximumLength(order.ShippingMethod, 50),
quantity = 1
};
//whether shipping is taxable
if (_taxSettings.ShippingIsTaxable)
{
//try to get tax code
var shippingTaxCategory = await _taxCategoryService.GetTaxCategoryByIdAsync(_taxSettings.ShippingTaxClassId);
shippingItem.taxCode = CommonHelper.EnsureMaximumLength(shippingTaxCategory?.Name, 25);
}
else
{
//if shipping is non-taxable, set it as exempt
shippingItem.exemptionCode = "Shipping-rate-non-taxable";
}
return shippingItem;
}
///
/// Create item lines for order checkout attributes
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the collection of item lines
///
protected async Task> CreateLinesForCheckoutAttributesAsync(Order order)
{
//get checkout attributes values
var attributeValues = _checkoutAttributeParser.ParseAttributeValues(order.CheckoutAttributesXml);
return await attributeValues.SelectManyAwait(async attributeWithValues =>
{
var attribute = attributeWithValues.attribute;
return (await attributeWithValues.values.SelectAwait(async value =>
{
//create line
var checkoutAttributeItem = new LineItemModel
{
amount = value.PriceAdjustment,
//item description
description = CommonHelper.EnsureMaximumLength($"{attribute.Name} ({value.Name})", 2096),
//whether the discount to the item was applied
discounted = order.OrderSubTotalDiscountExclTax > decimal.Zero,
//set checkout attribute name and value as item code
itemCode = CommonHelper.EnsureMaximumLength($"{attribute.Name}-{value.Name}", 50),
quantity = 1
};
//whether checkout attribute is tax exempt
if (attribute.IsTaxExempt)
checkoutAttributeItem.exemptionCode = "Attribute-non-taxable";
else
{
//or try to get tax code
var attributeTaxCategory = await _taxCategoryService.GetTaxCategoryByIdAsync(attribute.TaxCategoryId);
checkoutAttributeItem.taxCode = CommonHelper.EnsureMaximumLength(attributeTaxCategory?.Name, 25);
}
//whether entity use code is set
var entityUseCode = await _genericAttributeService.GetAttributeAsync(attribute, AvalaraTaxDefaults.EntityUseCodeAttribute);
checkoutAttributeItem.customerUsageType = CommonHelper.EnsureMaximumLength(entityUseCode, 25);
return checkoutAttributeItem;
}).ToListAsync()).ToAsyncEnumerable();
}).ToListAsync();
}
///
/// Prepare model tax exemption details
///
/// Model
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the model
///
protected async Task PrepareModelTaxExemptionAsync(CreateTransactionModel model, Customer customer)
{
if (customer.IsTaxExempt)
model.exemptionNo = CommonHelper.EnsureMaximumLength($"Exempt-customer-#{customer.Id}", 25);
else
{
var customerRole = (await _customerService.GetCustomerRolesAsync(customer)).FirstOrDefault(role => role.TaxExempt);
if (customerRole != null)
model.exemptionNo = CommonHelper.EnsureMaximumLength($"Exempt-{customerRole.Name}", 25);
}
var entityUseCode = await _genericAttributeService.GetAttributeAsync(customer, AvalaraTaxDefaults.EntityUseCodeAttribute);
if (!string.IsNullOrEmpty(entityUseCode))
model.customerUsageType = CommonHelper.EnsureMaximumLength(entityUseCode, 25);
else
{
entityUseCode = await (await _customerService.GetCustomerRolesAsync(customer))
.SelectAwait(async customerRole => await _genericAttributeService.GetAttributeAsync(customerRole, AvalaraTaxDefaults.EntityUseCodeAttribute))
.FirstOrDefaultAsync(code => !string.IsNullOrEmpty(code));
model.customerUsageType = CommonHelper.EnsureMaximumLength(entityUseCode, 25);
}
return model;
}
///
/// Get tax rates from the file
///
///
/// A task that represents the asynchronous operation
/// The task result contains the tax rates list
///
protected async Task> GetTaxRatesFromFileAsync()
{
//try to create file if doesn't exist
var filePath = _fileProvider.MapPath(AvalaraTaxDefaults.TaxRatesFilePath);
if (!_fileProvider.FileExists(filePath))
await DownloadTaxRatesAsync();
if (!_fileProvider.FileExists(filePath))
throw new NopException($"File {AvalaraTaxDefaults.TaxRatesFilePath} not found");
//get file lines
var text = await _fileProvider.ReadAllTextAsync(filePath, Encoding.UTF8);
if (string.IsNullOrEmpty(text))
throw new NopException($"File {AvalaraTaxDefaults.TaxRatesFilePath} is empty");
var lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
if (!lines.Any() || lines[0].Split(',').Length < 14)
throw new NopException($"Unsupported file {AvalaraTaxDefaults.TaxRatesFilePath} structure");
//prepare tax rates
var taxRates = lines.Skip(1).Select(line =>
{
try
{
var values = line.Split(',', StringSplitOptions.TrimEntries);
return new TaxRate
{
Zip = values[0], //ZIP_CODE
State = values[1], //STATE_ABBREV
County = values[2], //COUNTY_NAME
City = values[3], //CITY_NAME
StateTax = decimal.Parse(values[4], NumberStyles.Any, CultureInfo.InvariantCulture), //STATE_SALES_TAX
CountyTax = decimal.Parse(values[6], NumberStyles.Any, CultureInfo.InvariantCulture), //COUNTY_SALES_TAX
CityTax = decimal.Parse(values[8], NumberStyles.Any, CultureInfo.InvariantCulture), //CITY_SALES_TAX
TotalTax = decimal.Parse(values[10], NumberStyles.Any, CultureInfo.InvariantCulture), //TOTAL_SALES_TAX
ShippingTaxable = string.Equals(values[11], "y", StringComparison.InvariantCultureIgnoreCase), //TAX_SHIPPING_ALONE
ShippingAndHadlingTaxable = string.Equals(values[12], "y", StringComparison.InvariantCultureIgnoreCase) //TAX_SHIPPING_AND_HANDLING_TOGETHER
};
}
catch
{
return null;
}
}).Where(taxRate => taxRate is not null).ToList();
return taxRates;
}
#endregion
#region Certificates
///
/// Create or update the passed customer for the company
///
/// Customer
/// Selected company id
/// Whether the customer is already created
///
/// A task that represents the asynchronous operation
/// The task result contains the customer details
///
protected async Task CreateOrUpdateCustomerAsync(Customer customer, int companyId, bool customerExists)
{
var defaultAddress = new Address
{
Address1 = customer.StreetAddress,
Address2 = customer.StreetAddress2,
City = customer.City,
ZipPostalCode = customer.ZipPostalCode,
StateProvinceId = customer.StateProvinceId,
CountryId = customer.CountryId
};
var address = await MapAddressAsync(defaultAddress);
var model = new CustomerModel
{
companyId = companyId,
customerCode = customer.Id.ToString(),
alternateId = customer.CustomerGuid.ToString().ToLowerInvariant(),
name = await _customerService.GetCustomerFullNameAsync(customer),
emailAddress = customer.Email,
line1 = address.line1,
line2 = address.line2,
city = address.city,
postalCode = address.postalCode,
country = address.country,
region = address.region
};
var customerDetails = customerExists
? await ServiceClient.UpdateCustomerAsync(companyId, customer.Id.ToString(), model)
: (await ServiceClient.CreateCustomersAsync(companyId, [model]))?.FirstOrDefault();
return customerDetails;
}
#endregion
#region Item classification
///
/// Create classification model
///
/// Item classification
///
/// A task that represents the asynchronous operation
/// The task result contains the classification model
///
protected async Task CreateHSClassificationAsync(ItemClassification item)
{
var model = new HSClassificationModel();
var itemModel = new ItemClassificationModel();
var product = await _productService.GetProductByIdAsync(item.ProductId);
model.CountryOfDestination = (await _countryService.GetCountryByIdAsync(item.CountryId))?.TwoLetterIsoCode;
model.Id = $"{product.Id}-{model.CountryOfDestination}";
itemModel.CompanyId = _avalaraTaxSettings.CompanyId ?? 0;
itemModel.ItemCode = product.Sku;
itemModel.Description = $"{product.Name}. {product.ShortDescription}";
itemModel.ParentCode = product.ParentGroupedProductId > 0 ? product.ParentGroupedProductId.ToString() : "";
itemModel.Summary = product.FullDescription;
//If the product belongs to several categories, then we will ignore it and take only the first one from the list,
//since there is no way to transfer the entire list of categories where the product is included
var productCategories = await _categoryService.GetProductCategoriesByProductIdAsync(item.ProductId);
var category = await _categoryService.GetCategoryByIdAsync(productCategories[0].CategoryId);
itemModel.ItemGroup = await _categoryService.GetFormattedBreadCrumbAsync(category, separator: ">");
model.Item = itemModel;
return model;
}
#endregion
#endregion
#region Methods
#region Configuration
///
/// Ping service (test conection)
///
///
/// A task that represents the asynchronous operation
/// The task result contains the ping result
///
public async Task PingAsync()
{
return await HandleFunctionAsync(() => Task.FromResult(ServiceClient.Ping() ?? throw new NopException("No response from the service")));
}
///
/// Get account companies
///
///
/// A task that represents the asynchronous operation
/// The task result contains the list of companies
///
public async Task> GetAccountCompaniesAsync()
{
return await HandleFunctionAsync(() =>
{
var result = ServiceClient.QueryCompanies(null, null, null, null, null)
?? throw new NopException("No response from the service");
return Task.FromResult(result.value);
});
}
///
/// Get pre-defined entity use codes
///
///
/// A task that represents the asynchronous operation
/// The task result contains the list of entity use codes
///
public async Task> GetEntityUseCodesAsync()
{
return await HandleFunctionAsync(() =>
{
var result = ServiceClient.ListEntityUseCodes(null, null, null, null)
?? throw new NopException("No response from the service");
return Task.FromResult(result.value);
});
}
///
/// Get pre-defined tax code types
///
///
/// A task that represents the asynchronous operation
/// The task result contains the key-value pairs of tax code types
///
public async Task> GetTaxCodeTypesAsync()
{
return await HandleFunctionAsync(() =>
{
var result = ServiceClient.ListTaxCodeTypes(null, null)
?? throw new NopException("No response from the service");
return Task.FromResult(result.types);
});
}
///
/// Import tax codes from Avalara services
///
///
/// A task that represents the asynchronous operation
/// The task result contains the number of imported tax codes; null in case of error
///
public async Task ImportTaxCodesAsync()
{
return await HandleFunctionAsync(async () =>
{
//get Avalara pre-defined system tax codes (only active)
var systemTaxCodes = await ServiceClient.ListTaxCodesAsync("isActive eq true", null, null, null)
?? throw new NopException("No response from the service");
if (!systemTaxCodes.value?.Any() ?? true)
return null;
//get existing tax categories
var existingTaxCategories = (await _taxCategoryService.GetAllTaxCategoriesAsync())
.Select(taxCategory => taxCategory.Name)
.ToList();
//remove duplicates
var taxCodesToImport = systemTaxCodes.value
.Where(taxCode => !string.IsNullOrEmpty(taxCode?.taxCode) && !existingTaxCategories.Contains(taxCode.taxCode))
.ToList();
var importedTaxCodesNumber = 0;
foreach (var taxCode in taxCodesToImport)
{
//create new tax category
var taxCategory = new TaxCategory { Name = taxCode.taxCode };
await _taxCategoryService.InsertTaxCategoryAsync(taxCategory);
//save description and type
if (!string.IsNullOrEmpty(taxCode.description))
await _genericAttributeService.SaveAttributeAsync(taxCategory, AvalaraTaxDefaults.TaxCodeDescriptionAttribute, taxCode.description);
if (!string.IsNullOrEmpty(taxCode.taxCodeTypeId))
await _genericAttributeService.SaveAttributeAsync(taxCategory, AvalaraTaxDefaults.TaxCodeTypeAttribute, taxCode.taxCodeTypeId);
importedTaxCodesNumber++;
}
return importedTaxCodesNumber;
});
}
///
/// Export current tax codes to Avalara services
///
///
/// A task that represents the asynchronous operation
/// The task result contains the number of exported tax codes; null in case of error
///
public async Task ExportTaxCodesAsync()
{
return await HandleFunctionAsync(async () =>
{
if (string.IsNullOrEmpty(_avalaraTaxSettings.CompanyCode) || _avalaraTaxSettings.CompanyCode.Equals(Guid.Empty.ToString()))
throw new NopException("Company not selected");
//get selected company
var selectedCompany = (await GetAccountCompaniesAsync())
?.FirstOrDefault(company => _avalaraTaxSettings.CompanyCode.Equals(company?.companyCode))
?? throw new NopException("Failed to retrieve company");
//get existing tax codes (only active)
var taxCodes = await ServiceClient.ListTaxCodesByCompanyAsync(selectedCompany.id, "isActive eq true", null, null, null, null)
?? throw new NopException("No response from the service");
var existingTaxCodes = taxCodes.value?.Select(taxCode => taxCode.taxCode).ToList() ?? new List();
//prepare tax codes to export
var taxCodesToExport = await (await _taxCategoryService.GetAllTaxCategoriesAsync()).SelectAwait(async taxCategory => new TaxCodeModel
{
createdDate = DateTime.UtcNow,
description = CommonHelper.EnsureMaximumLength(taxCategory.Name, 255),
isActive = true,
taxCode = CommonHelper.EnsureMaximumLength(taxCategory.Name, 25),
taxCodeTypeId = CommonHelper.EnsureMaximumLength(await _genericAttributeService
.GetAttributeAsync(taxCategory, AvalaraTaxDefaults.TaxCodeTypeAttribute) ?? "P", 2)
}).Where(taxCode => !string.IsNullOrEmpty(taxCode.taxCode)).ToListAsync();
//add Avalara pre-defined system tax codes
var systemTaxCodesResult = await ServiceClient.ListTaxCodesAsync("isActive eq true", null, null, null)
?? throw new NopException("No response from the service");
var systemTaxCodes = systemTaxCodesResult.value?.Select(taxCode => taxCode.taxCode).ToList() ?? new List();
existingTaxCodes.AddRange(systemTaxCodes);
//remove duplicates
taxCodesToExport = taxCodesToExport.Where(taxCode => !existingTaxCodes.Contains(taxCode.taxCode)).Distinct().ToList();
//export tax codes
if (!taxCodesToExport.Any())
return 0;
//create items and get the result
var createdTaxCodes = await ServiceClient.CreateTaxCodesAsync(selectedCompany.id, taxCodesToExport)
?? throw new NopException("No response from the service");
//display results
var result = createdTaxCodes?.Count;
if (result.HasValue && result > 0)
return result.Value;
return null;
});
}
///
/// Delete pre-defined system tax codes
///
///
/// A task that represents the asynchronous operation
/// The task result contains the result
///
public async Task DeleteSystemTaxCodesAsync()
{
return await HandleFunctionAsync(async () =>
{
//get Avalara pre-defined system tax codes (only active)
var systemTaxCodesResult = await ServiceClient.ListTaxCodesAsync("isActive eq true", null, null, null)
?? throw new NopException("No response from the service");
var systemTaxCodes = systemTaxCodesResult.value?.Select(taxCode => taxCode.taxCode).ToList();
if (!systemTaxCodes?.Any() ?? true)
return false;
//prepare tax categories to delete
var categoriesIds = await _taxCategoryRepository.Table
.Where(taxCategory => systemTaxCodes.Contains(taxCategory.Name))
.Select(taxCategory => taxCategory.Id)
.ToListAsync();
//delete tax categories
await _taxCategoryRepository.DeleteAsync(taxCategory => categoriesIds.Contains(taxCategory.Id));
await _staticCacheManager.RemoveByPrefixAsync(NopEntityCacheDefaults.Prefix);
//delete generic attributes
await _genericAttributeRepository
.DeleteAsync(attribute => attribute.KeyGroup == nameof(TaxCategory) && categoriesIds.Contains(attribute.EntityId));
return true;
});
}
///
/// Delete generic attributes used in the plugin
///
/// A task that represents the asynchronous operation
public async Task DeleteAttributesAsync()
{
await DeleteSystemTaxCodesAsync();
await _genericAttributeRepository.DeleteAsync(attribute => attribute.Key == AvalaraTaxDefaults.EntityUseCodeAttribute ||
attribute.Key == AvalaraTaxDefaults.TaxCodeTypeAttribute || attribute.Key == AvalaraTaxDefaults.TaxCodeDescriptionAttribute);
}
///
/// Export items (products) with the passed ids to Avalara services
///
///
/// A task that represents the asynchronous operation
/// The task result contains the number of exported items; null in case of error
///
public async Task ExportProductsAsync(string selectedIds)
{
return await HandleFunctionAsync(async () =>
{
if (string.IsNullOrEmpty(_avalaraTaxSettings.CompanyCode) || _avalaraTaxSettings.CompanyCode.Equals(Guid.Empty.ToString()))
throw new NopException("Company not selected");
//get selected company
var selectedCompany = (await GetAccountCompaniesAsync())
?.FirstOrDefault(company => _avalaraTaxSettings.CompanyCode.Equals(company?.companyCode))
?? throw new NopException("Failed to retrieve company");
//get existing items
var items = await ServiceClient.ListItemsByCompanyAsync(selectedCompany.id, null, null, null, null, null, null)
?? throw new NopException("No response from the service");
//return the paginated and filtered list
var existingItemCodes = items.value?.Select(item => item.itemCode).ToList() ?? new List();
//prepare exported items
var productIds = selectedIds?.Split(_separator, StringSplitOptions.RemoveEmptyEntries).Select(id => Convert.ToInt32(id)).ToArray();
var exportedItems = new List();
foreach (var product in await _productService.GetProductsByIdsAsync(productIds))
{
//find product combinations
var combinations = (await _productAttributeService.GetAllProductAttributeCombinationsAsync(product.Id))
.Where(combination => !string.IsNullOrEmpty(combination.Sku));
//export items with specified SKU only
if (string.IsNullOrEmpty(product.Sku) && !combinations.Any())
continue;
//prepare common properties
var taxCategory = await _taxCategoryService.GetTaxCategoryByIdAsync(product.TaxCategoryId);
var taxCode = CommonHelper.EnsureMaximumLength(taxCategory?.Name, 25);
var description = CommonHelper.EnsureMaximumLength(product.Name, 255);
//add the product as exported item
if (!string.IsNullOrEmpty(product.Sku))
{
exportedItems.Add(new ItemModel
{
createdDate = DateTime.UtcNow,
description = description,
itemCode = CommonHelper.EnsureMaximumLength(product.Sku, 50),
taxCode = taxCode
});
}
//add product combinations
exportedItems.AddRange(combinations.Select(combination => new ItemModel
{
createdDate = DateTime.UtcNow,
description = description,
itemCode = CommonHelper.EnsureMaximumLength(combination.Sku, 50),
taxCode = taxCode
}));
}
//remove duplicates
exportedItems = exportedItems.Where(item => !existingItemCodes.Contains(item.itemCode)).Distinct().ToList();
//export items
if (!exportedItems.Any())
return 0;
//create items and get the result
var createdItems = await ServiceClient.CreateItemsAsync(selectedCompany.id, exportedItems)
?? throw new NopException("No response from the service");
//display results
var result = createdItems?.Count;
if (result.HasValue && result > 0)
return result.Value;
return null;
});
}
#endregion
#region Validation
///
/// Resolve the passed address against Avalara's address-validation system
///
/// Address to validate
///
/// A task that represents the asynchronous operation
/// The task result contains the validated address
///
public async Task ValidateAddressAsync(Address address)
{
return (await HandleFunctionAsync(async () => await ServiceClient.ResolveAddressPostAsync(new AddressValidationInfo
{
city = CommonHelper.EnsureMaximumLength(address.City, 50),
country = CommonHelper.EnsureMaximumLength((await _countryService.GetCountryByAddressAsync(address))?.TwoLetterIsoCode, 2),
line1 = CommonHelper.EnsureMaximumLength(address.Address1, 50),
line2 = CommonHelper.EnsureMaximumLength(address.Address2, 100),
postalCode = CommonHelper.EnsureMaximumLength(address.ZipPostalCode, 11),
region = CommonHelper.EnsureMaximumLength((await _stateProvinceService.GetStateProvinceByAddressAsync(address))?.Abbreviation, 3),
textCase = TextCase.Mixed
}) ?? throw new NopException("No response from the service")));
}
#endregion
#region Tax calculation
///
/// Create test tax transaction
///
/// Tax address
///
/// A task that represents the asynchronous operation
/// The task result contains the ransaction
///
public async Task CreateTestTaxTransactionAsync(Address address)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.UseTaxRateTables)
{
var taxRates = await GetTaxRatesFromFileAsync();
var taxRate = taxRates.FirstOrDefault(record => address.ZipPostalCode?.StartsWith(record.Zip) ?? false);
if (taxRate?.TotalTax is null)
throw new NopException($"No rate found for zip code {address.ZipPostalCode}");
var summary = new List();
if (!string.IsNullOrEmpty(taxRate.State))
summary.Add(new() { jurisName = taxRate.State, rate = taxRate.StateTax });
if (!string.IsNullOrEmpty(taxRate.County))
summary.Add(new() { jurisName = taxRate.County, rate = taxRate.CountyTax });
if (!string.IsNullOrEmpty(taxRate.City))
summary.Add(new() { jurisName = taxRate.City, rate = taxRate.CityTax });
return new TransactionModel { totalTax = taxRate.TotalTax * 100, summary = summary };
}
var customer = await _workContext.GetCurrentCustomerAsync();
//create tax transaction for a simplified item and without saving
var model = await PrepareTransactionModelAsync(address, customer.Id.ToString(), DocumentType.SalesOrder);
model.lines = [new LineItemModel { amount = 100, quantity = 1 }];
return CreateTransaction(model);
});
}
///
/// Create transaction to get tax rate
///
/// Tax rate request
///
/// A task that represents the asynchronous operation
/// The task result contains the ransaction
///
public async Task GetTaxRateAsync(TaxRateRequest taxRateRequest)
{
if (_avalaraTaxSettings.UseTaxRateTables)
{
var key = _staticCacheManager
.PrepareKeyForDefaultCache(AvalaraTaxDefaults.TaxRateByZipCacheKey, taxRateRequest.Address.ZipPostalCode);
return await _staticCacheManager.GetAsync(key, async () => await HandleFunctionAsync(async () =>
{
var taxRates = await GetTaxRatesFromFileAsync();
var taxRate = taxRates.FirstOrDefault(record => taxRateRequest.Address.ZipPostalCode?.StartsWith(record.Zip) ?? false);
return taxRate?.TotalTax * 100;
}));
}
//prepare cache key
var address = await _addressService.GetAddressByIdAsync(taxRateRequest.Address.Id);
var customer = taxRateRequest.Customer ?? await _workContext.GetCurrentCustomerAsync();
var taxCategoryId = taxRateRequest.TaxCategoryId > 0
? taxRateRequest.TaxCategoryId
: taxRateRequest.Product?.TaxCategoryId ?? 0;
var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(AvalaraTaxDefaults.TaxRateCacheKey,
_avalaraTaxSettings.GetTaxRateByAddressOnly ? null : customer,
_avalaraTaxSettings.GetTaxRateByAddressOnly ? 0 : taxCategoryId,
taxRateRequest.Address.Address1,
taxRateRequest.Address.City,
taxRateRequest.Address.StateProvinceId ?? 0,
taxRateRequest.Address.CountryId ?? 0,
taxRateRequest.Address.ZipPostalCode);
if (_avalaraTaxSettings.GetTaxRateByAddressOnly && _avalaraTaxSettings.TaxRateByAddressCacheTime > 0)
cacheKey.CacheTime = _avalaraTaxSettings.TaxRateByAddressCacheTime;
//get tax rate
return await _staticCacheManager.GetAsync(cacheKey, async () =>
{
return await HandleFunctionAsync(async () =>
{
//create tax transaction for a single item and without saving
var model = await PrepareTransactionModelAsync(address, customer.Id.ToString(), DocumentType.SalesOrder);
var taxCategory = await _taxCategoryService.GetTaxCategoryByIdAsync(taxCategoryId);
model.lines =
[
new LineItemModel
{
amount = 100,
quantity = 1,
itemCode = CommonHelper.EnsureMaximumLength(taxRateRequest.Product?.Sku, 50),
taxCode = CommonHelper.EnsureMaximumLength(taxCategory?.Name, 25),
exemptionCode = !_avalaraTaxSettings.GetTaxRateByAddressOnly && (taxRateRequest.Product?.IsTaxExempt ?? false)
? CommonHelper.EnsureMaximumLength($"Exempt-product-#{taxRateRequest.Product.Id}", 25)
: string.Empty
}
];
//prepare tax exemption
if (!_avalaraTaxSettings.GetTaxRateByAddressOnly)
await PrepareModelTaxExemptionAsync(model, customer);
var transaction = CreateTransaction(model);
//we return the tax total, since we used the amount of 100 when requesting, so the total is the same as the rate
return transaction?.totalTax;
});
});
}
///
/// Create transaction to get tax total for the passed request
///
/// Tax total request
///
/// A task that represents the asynchronous operation
/// The task result contains the ransaction
///
public async Task CreateTaxTotalTransactionAsync(TaxTotalRequest taxTotalRequest)
{
return await HandleFunctionAsync(async () =>
{
//create dummy order to create tax transaction
var customer = taxTotalRequest.Customer;
var order = new Order { CustomerId = customer.Id };
//addresses
await PrepareOrderAddressesAsync(customer, order, taxTotalRequest.StoreId);
//checkout attributes
order.CheckoutAttributesXml = await _genericAttributeService
.GetAttributeAsync(customer, NopCustomerDefaults.CheckoutAttributes, taxTotalRequest.StoreId);
//shipping method
order.ShippingMethod = (await _genericAttributeService
.GetAttributeAsync(customer, NopCustomerDefaults.SelectedShippingOptionAttribute, taxTotalRequest.StoreId))?.Name;
order.OrderShippingExclTax = (await _orderTotalCalculationService.GetShoppingCartShippingTotalAsync(taxTotalRequest.ShoppingCart, false)).shippingTotal ?? 0;
//payment method
if (taxTotalRequest.UsePaymentMethodAdditionalFee)
{
order.PaymentMethodSystemName = await _genericAttributeService
.GetAttributeAsync(customer, NopCustomerDefaults.SelectedPaymentMethodAttribute, taxTotalRequest.StoreId);
if (!string.IsNullOrEmpty(order.PaymentMethodSystemName))
order.PaymentMethodAdditionalFeeExclTax = await _paymentService.GetAdditionalHandlingFeeAsync(taxTotalRequest.ShoppingCart, order.PaymentMethodSystemName);
}
//discount amount
var (orderSubTotalDiscountExclTax, _, _, _, _) = await _orderTotalCalculationService.GetShoppingCartSubTotalAsync(taxTotalRequest.ShoppingCart, false);
order.OrderSubTotalDiscountExclTax = orderSubTotalDiscountExclTax;
//create dummy order items
var orderItems = await taxTotalRequest.ShoppingCart.SelectAwait(async cartItem => new OrderItem
{
AttributesXml = cartItem.AttributesXml,
ProductId = cartItem.ProductId,
Quantity = cartItem.Quantity,
PriceExclTax = (await _shoppingCartService.GetSubTotalAsync(cartItem, true)).subTotal
}).ToListAsync();
//prepare transaction model
var address = await GetTaxAddressAsync(order);
var model = await PrepareTransactionModelAsync(address, customer.Id.ToString(), DocumentType.SalesOrder);
model.email = CommonHelper.EnsureMaximumLength(customer.Email, 50);
model.discount = order.OrderSubTotalDiscountExclTax;
//set purchased item lines
model.lines = await GetItemLinesAsync(order, orderItems, address.CountryId);
//set whole request tax exemption
await PrepareModelTaxExemptionAsync(model, customer);
return CreateTransaction(model);
});
}
///
/// Create tax transaction for the placed order
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the ransaction
///
public async Task CreateOrderTaxTransactionAsync(Order order)
{
return await HandleFunctionAsync(async () =>
{
//prepare transaction model
var address = await GetTaxAddressAsync(order);
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
var model = await PrepareTransactionModelAsync(address, customer.Id.ToString(), DocumentType.SalesInvoice);
model.email = CommonHelper.EnsureMaximumLength(customer.Email, 50);
model.code = CommonHelper.EnsureMaximumLength(order.CustomOrderNumber, 50);
model.commit = _avalaraTaxSettings.CommitTransactions;
model.discount = order.OrderSubTotalDiscountExclTax;
//set purchased item lines
var orderItems = await _orderService.GetOrderItemsAsync(order.Id);
model.lines = await GetItemLinesAsync(order, orderItems, address.CountryId);
//set whole request tax exemption
await PrepareModelTaxExemptionAsync(model, customer);
return CreateTransaction(model);
});
}
///
/// Void tax transaction
///
/// Order
/// A task that represents the asynchronous operation
public async Task VoidTaxTransactionAsync(Order order)
{
await HandleFunctionAsync(() =>
{
if (string.IsNullOrEmpty(_avalaraTaxSettings.CompanyCode) || _avalaraTaxSettings.CompanyCode.Equals(Guid.Empty.ToString()))
throw new NopException("Company not selected");
var model = new VoidTransactionModel { code = VoidReasonCode.DocVoided };
var transaction = ServiceClient.VoidTransaction(_avalaraTaxSettings.CompanyCode, order.CustomOrderNumber, null, null, model)
?? throw new NopException("No response from the service");
return Task.FromResult(transaction);
});
}
///
/// Delete tax transaction
///
/// Order
/// A task that represents the asynchronous operation
public async Task DeleteTaxTransactionAsync(Order order)
{
await HandleFunctionAsync(() =>
{
if (string.IsNullOrEmpty(_avalaraTaxSettings.CompanyCode) || _avalaraTaxSettings.CompanyCode.Equals(Guid.Empty.ToString()))
throw new NopException("Company not selected");
var model = new VoidTransactionModel { code = VoidReasonCode.DocDeleted };
var transaction = ServiceClient.VoidTransaction(_avalaraTaxSettings.CompanyCode, order.CustomOrderNumber, null, null, model)
?? throw new NopException("No response from the service");
return Task.FromResult(transaction);
});
}
///
/// Refund tax transaction
///
/// Order
/// Amount to refund
/// A task that represents the asynchronous operation
public async Task RefundTaxTransactionAsync(Order order, decimal amountToRefund)
{
await HandleFunctionAsync(() =>
{
if (string.IsNullOrEmpty(_avalaraTaxSettings.CompanyCode) || _avalaraTaxSettings.CompanyCode.Equals(Guid.Empty.ToString()))
throw new NopException("Company not selected");
//first try to get saved tax transaction
var transaction = ServiceClient.GetTransactionByCodeAndType(_avalaraTaxSettings.CompanyCode, order.CustomOrderNumber, DocumentType.SalesInvoice, null)
?? throw new NopException("No response from the service");
//create refund transaction model
var model = new RefundTransactionModel
{
referenceCode = CommonHelper.EnsureMaximumLength(transaction.code, 50),
refundDate = transaction.date ?? DateTime.UtcNow,
refundType = RefundType.Full
};
//whether it's a partial refund
var isPartialRefund = amountToRefund < order.OrderTotal;
if (isPartialRefund)
{
model.refundType = RefundType.Percentage;
model.refundPercentage = amountToRefund / (order.OrderTotal - order.OrderTax) * 100;
}
transaction = ServiceClient.RefundTransaction(_avalaraTaxSettings.CompanyCode, transaction.code, null, null, null, model)
?? throw new NopException("No response from the service");
return Task.FromResult(transaction);
});
}
///
/// Download a file listing tax rates by postal code
///
/// A task that represents the asynchronous operation
public async Task DownloadTaxRatesAsync()
{
await HandleFunctionAsync(async () =>
{
var file = ServiceClient.DownloadTaxRatesByZipCode(DateTime.UtcNow, null)
?? throw new NopException("No response from the service");
var filePath = _fileProvider.MapPath(AvalaraTaxDefaults.TaxRatesFilePath);
await _fileProvider.WriteAllBytesAsync(filePath, file.Data);
return true;
});
}
#endregion
#region Certificates
///
/// Checks whether the company is configured to use exemption certificates
///
/// Whether to request the certificate setup
///
/// A task that represents the asynchronous operation
/// The task result contains the result of check
///
public async Task GetCertificateSetupStatusAsync(bool request = false)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
var provisionStatus = request
? await ServiceClient.RequestCertificateSetupAsync(_avalaraTaxSettings.CompanyId.Value)
: await ServiceClient.GetCertificateSetupAsync(_avalaraTaxSettings.CompanyId.Value)
?? throw new NopException("Failed to get certificate setup status");
if (provisionStatus.status == CertCaptureProvisionStatus.NotProvisioned)
return default;
return provisionStatus.status == CertCaptureProvisionStatus.Provisioned;
});
}
///
/// Create a new authorization token to launch the certificates services
///
/// Customer for which token is creating
///
/// A task that represents the asynchronous operation
/// The task result contains the generated token
///
public async Task CreateTokenAsync(Customer customer)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
//no need to log the error if the customer is not created yet
var customerExists = false;
try
{
customerExists = await ServiceClient
.GetCustomerAsync(_avalaraTaxSettings.CompanyId.Value, customer.Id.ToString(), null) is not null;
}
catch { }
if (!customerExists)
await CreateOrUpdateCustomerAsync(customer, _avalaraTaxSettings.CompanyId.Value, customerExists);
var model = new CreateECommerceTokenInputModel { customerNumber = customer.Id.ToString() };
return (await ServiceClient.CreateECommerceTokenAsync(_avalaraTaxSettings.CompanyId.Value, model))?.token
?? throw new NopException("Failed to get token");
});
}
///
/// Get the certificate exposure zones defined by the company
///
///
/// A task that represents the asynchronous operation
/// The task result contains the list of exposure zones
///
public async Task> GetExposureZonesAsync()
{
return await HandleFunctionAsync(async () =>
{
var result = await ServiceClient.ListCertificateExposureZonesAsync(null, null, null, null)
?? throw new NopException("Failed to get exposure zones");
return result.value;
});
}
///
/// Create or update the passed customer for the company
///
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the customer details
///
public async Task CreateOrUpdateCustomerAsync(Customer customer)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
//no need to log the error if the customer is not created yet
var customerExists = false;
try
{
customerExists = await ServiceClient
.GetCustomerAsync(_avalaraTaxSettings.CompanyId.Value, customer.Id.ToString(), null) is not null;
}
catch { }
return await CreateOrUpdateCustomerAsync(customer, _avalaraTaxSettings.CompanyId.Value, customerExists)
?? throw new NopException("Failed to update customer details");
});
}
///
/// Delete a customer with the passed identifier
///
/// Customer id
///
/// A task that represents the asynchronous operation
/// The task result contains the customer details
///
public async Task DeleteCustomerAsync(int customerId)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
return await ServiceClient.DeleteCustomerAsync(_avalaraTaxSettings.CompanyId.Value, customerId.ToString())
?? throw new NopException("Failed to delete customer");
});
}
///
/// Get valid certificates linked to a customer in a particular country and region
///
/// Customer
/// Current store id
///
/// A task that represents the asynchronous operation
/// The task result contains the list of certificates
///
public async Task GetValidCertificatesAsync(Customer customer, int storeId)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
//create dummy order to get selected address
var order = new Order { CustomerId = customer.Id };
await PrepareOrderAddressesAsync(customer, order, storeId);
var address = await GetTaxAddressAsync(order);
var shipTo = await MapAddressAsync(address);
//check exemption status
var exemptionStatus = await ServiceClient
.ListValidCertificatesForCustomerAsync(_avalaraTaxSettings.CompanyId.Value, customer.Id.ToString(), shipTo.country, shipTo.region)
?? throw new NopException("Failed to get customer's certificates");
var exempt = string.Equals(exemptionStatus.status, "Exempt", StringComparison.InvariantCultureIgnoreCase);
return exempt ? exemptionStatus.certificate : null;
});
}
///
/// Get all certificates linked to a customer
///
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the list of certificates
///
public async Task> GetCustomerCertificatesAsync(Customer customer)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
var certificates = await ServiceClient
.ListCertificatesForCustomerAsync(_avalaraTaxSettings.CompanyId.Value, customer.Id.ToString(), null, null, null, null, null)
?? throw new NopException("Failed to get customer's certificates");
return certificates.value;
});
}
///
/// Download a PDF file for the certificate
///
/// The unique ID number of the certificate
///
/// A task that represents the asynchronous operation
/// The task result contains the file details
///
public async Task DownloadCertificateAsync(int certificateId)
{
return await HandleFunctionAsync(() =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
var file = ServiceClient
.DownloadCertificateImage(_avalaraTaxSettings.CompanyId.Value, certificateId, null, CertificatePreviewType.Pdf)
?? throw new NopException("Failed to download certificate");
return Task.FromResult(file);
});
}
///
/// Get an existing invitation to upload certificates on the external website
///
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the URL to redirect customer
///
public async Task GetInvitationAsync(Customer customer)
{
return await HandleFunctionAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
//create invitation for customer
var invitationModel = new List
{
new() { deliveryMethod = CertificateRequestDeliveryMethod.Download }
};
var invitation = (await ServiceClient
.CreateCertExpressInvitationAsync(_avalaraTaxSettings.CompanyId.Value, customer.Id.ToString(), invitationModel))
?.FirstOrDefault()?.invitation
?? throw new NopException("Failed to get invitation");
return invitation.requestLink;
});
}
#endregion
#region Item classification
///
/// Get item classification
///
///
/// A task that represents the asynchronous operation
/// The task result contains the classification results
///
public async Task<(HSClassificationModel model, string Error)> ClassificationProductsAsync(ItemClassification item)
{
return await HandleFunctionExAsync(async () =>
{
if (_avalaraTaxSettings.CompanyId is null)
throw new NopException("Company not selected");
var companyId = _avalaraTaxSettings.CompanyId.ToString();
var classificationModel = await CreateHSClassificationAsync(item);
var classification = await _itemClassificationHttpClient
.RequestAsync(new CreateHSClassificationRequest(companyId)
{
Id = classificationModel.Id,
CountryOfDestination = classificationModel.CountryOfDestination,
Item = classificationModel.Item
});
return classification;
});
}
///
/// Handle webhook request
///
/// HTTP request
///
/// A task that represents the asynchronous operation
public async Task HandleItemClassificationWebhookAsync(Microsoft.AspNetCore.Http.HttpRequest request)
{
await HandleFunctionAsync(async () =>
{
using var streamReader = new StreamReader(request.Body);
var requestContent = await streamReader.ReadToEndAsync();
if (string.IsNullOrEmpty(requestContent))
throw new NopException("Webhook request content is empty");
//log request
var stringBuilder = new StringBuilder();
stringBuilder.Append(nameof(request.Headers));
foreach (var header in request.Headers)
{
stringBuilder.AppendLine($"{header.Key}: {header.Value}");
}
stringBuilder.AppendLine($"{nameof(request.Path)}: {request.Path}");
stringBuilder.AppendLine($"{nameof(request.QueryString)}: {request.QueryString}");
stringBuilder.AppendLine($"{nameof(request.Body)}: {requestContent}");
await _logger.InformationAsync($"{AvalaraTaxDefaults.SystemName} webhook request info. {stringBuilder}");
//get webhook message
var message = JsonConvert.DeserializeObject(requestContent);
if (!string.IsNullOrEmpty(message.HsCode))
{
var itemClassification = await _itemClassificationService.GetItemClassificationByRequestIdAsync(message.Id);
if (itemClassification is not null)
{
itemClassification.HSCode = message.HsCode;
itemClassification.UpdatedOnUtc = DateTime.UtcNow;
await _itemClassificationService.UpdateItemClassificationAsync(itemClassification);
}
}
return true;
});
}
#endregion
///
/// Dispose object
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// Protected implementation of Dispose pattern.
protected void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
if (_serviceClient != null)
_serviceClient.CallCompleted -= OnCallCompleted;
}
_disposed = true;
}
#endregion
#region Properties
///
/// Gets client that connects to Avalara services
///
protected AvaTaxClient ServiceClient
{
get
{
if (_serviceClient == null)
{
//create a client with credentials
_serviceClient = new AvaTaxClient(AvalaraTaxDefaults.ApplicationName,
AvalaraTaxDefaults.ApplicationVersion, Environment.MachineName,
_avalaraTaxSettings.UseSandbox ? AvaTaxEnvironment.Sandbox : AvaTaxEnvironment.Production)
.WithSecurity(_avalaraTaxSettings.AccountId, _avalaraTaxSettings.LicenseKey);
//invoke method after each request to services completed
if (_avalaraTaxSettings.EnableLogging)
_serviceClient.CallCompleted += OnCallCompleted;
}
return _serviceClient;
}
}
#endregion
}