Try your search with a different keyword or use * as a wildcard.
using System.Text.RegularExpressions;
using Nop.Core;
using Nop.Core.Domain.Catalog;
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.Events;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Logging;
using Nop.Services.Tax.Events;
namespace Nop.Services.Tax;
///
/// Tax service
///
public partial class TaxService : ITaxService
{
#region Fields
protected readonly AddressSettings _addressSettings;
protected readonly CustomerSettings _customerSettings;
protected readonly IAddressService _addressService;
protected readonly ICheckVatService _checkVatService;
protected readonly ICountryService _countryService;
protected readonly ICustomerService _customerService;
protected readonly IEventPublisher _eventPublisher;
protected readonly IGenericAttributeService _genericAttributeService;
protected readonly IGeoLookupService _geoLookupService;
protected readonly ILogger _logger;
protected readonly IStateProvinceService _stateProvinceService;
protected readonly IStoreContext _storeContext;
protected readonly ITaxPluginManager _taxPluginManager;
protected readonly IWebHelper _webHelper;
protected readonly IWorkContext _workContext;
protected readonly ShippingSettings _shippingSettings;
protected readonly TaxSettings _taxSettings;
#endregion
#region Ctor
public TaxService(AddressSettings addressSettings,
CustomerSettings customerSettings,
IAddressService addressService,
ICheckVatService checkVatService,
ICountryService countryService,
ICustomerService customerService,
IEventPublisher eventPublisher,
IGenericAttributeService genericAttributeService,
IGeoLookupService geoLookupService,
ILogger logger,
IStateProvinceService stateProvinceService,
IStoreContext storeContext,
ITaxPluginManager taxPluginManager,
IWebHelper webHelper,
IWorkContext workContext,
ShippingSettings shippingSettings,
TaxSettings taxSettings)
{
_addressSettings = addressSettings;
_customerSettings = customerSettings;
_addressService = addressService;
_checkVatService = checkVatService;
_countryService = countryService;
_customerService = customerService;
_eventPublisher = eventPublisher;
_genericAttributeService = genericAttributeService;
_geoLookupService = geoLookupService;
_logger = logger;
_stateProvinceService = stateProvinceService;
_storeContext = storeContext;
_taxPluginManager = taxPluginManager;
_webHelper = webHelper;
_workContext = workContext;
_shippingSettings = shippingSettings;
_taxSettings = taxSettings;
}
#endregion
#region Utilities
///
/// Gets a default tax address
///
///
/// A task that represents the asynchronous operation
/// The task result contains the address
///
protected virtual async Task LoadDefaultTaxAddressAsync()
{
var addressId = _taxSettings.DefaultTaxAddressId;
return await _addressService.GetAddressByIdAsync(addressId);
}
///
/// Gets or sets a pickup point address for tax calculation
///
/// Pickup point
///
/// A task that represents the asynchronous operation
/// The task result contains the address
///
protected virtual async Task LoadPickupPointTaxAddressAsync(PickupPoint pickupPoint)
{
ArgumentNullException.ThrowIfNull(pickupPoint);
var country = await _countryService.GetCountryByTwoLetterIsoCodeAsync(pickupPoint.CountryCode);
var state = await _stateProvinceService.GetStateProvinceByAbbreviationAsync(pickupPoint.StateAbbreviation, country?.Id);
return new Address
{
CountryId = country?.Id ?? 0,
StateProvinceId = state?.Id ?? 0,
County = pickupPoint.County,
City = pickupPoint.City,
Address1 = pickupPoint.Address,
ZipPostalCode = pickupPoint.ZipPostalCode
};
}
///
/// Prepare request to get tax rate
///
/// Product
/// Tax category identifier
/// Customer
/// Price
///
/// A task that represents the asynchronous operation
/// The task result contains the package for tax calculation
///
protected virtual async Task PrepareTaxRateRequestAsync(Product product, int taxCategoryId, Customer customer, decimal price)
{
ArgumentNullException.ThrowIfNull(customer);
var store = await _storeContext.GetCurrentStoreAsync();
var taxRateRequest = new TaxRateRequest
{
Customer = customer,
Product = product,
Price = price,
TaxCategoryId = taxCategoryId > 0 ? taxCategoryId : product?.TaxCategoryId ?? 0,
CurrentStoreId = store.Id
};
var basedOn = _taxSettings.TaxBasedOn;
//tax is based on pickup point address
if (_taxSettings.TaxBasedOnPickupPointAddress && _shippingSettings.AllowPickupInStore)
{
var pickupPoint = await _genericAttributeService.GetAttributeAsync(customer,
NopCustomerDefaults.SelectedPickupPointAttribute, store.Id);
if (pickupPoint != null)
{
taxRateRequest.Address = await LoadPickupPointTaxAddressAsync(pickupPoint);
return taxRateRequest;
}
}
var autodetectedCountry = false;
var detectedAddress = new Address
{
CreatedOnUtc = DateTime.UtcNow
};
if (basedOn == TaxBasedOn.BillingAddress && customer.BillingAddressId == null ||
basedOn == TaxBasedOn.ShippingAddress && customer.ShippingAddressId == null)
{
if (_taxSettings.AutomaticallyDetectCountry)
{
var ipAddress = _webHelper.GetCurrentIpAddress();
var countryIsoCode = _geoLookupService.LookupCountryIsoCode(ipAddress);
var country = await _countryService.GetCountryByTwoLetterIsoCodeAsync(countryIsoCode);
if (country != null)
{
detectedAddress.CountryId = country.Id;
autodetectedCountry = true;
}
else
basedOn = TaxBasedOn.DefaultAddress;
}
else
basedOn = TaxBasedOn.DefaultAddress;
}
taxRateRequest.Address = basedOn switch
{
TaxBasedOn.BillingAddress => autodetectedCountry ? detectedAddress : await _customerService.GetCustomerBillingAddressAsync(customer),
TaxBasedOn.ShippingAddress => autodetectedCountry ? detectedAddress : await _customerService.GetCustomerShippingAddressAsync(customer),
_ => await LoadDefaultTaxAddressAsync(),
};
return taxRateRequest;
}
///
/// Calculated price
///
/// Price
/// Percent
/// Increase
/// New price
protected virtual decimal CalculatePrice(decimal price, decimal percent, bool increase)
{
if (percent == decimal.Zero)
return price;
decimal result;
if (increase)
result = price * (1 + percent / 100);
else
result = price - price / (100 + percent) * percent;
return result;
}
///
/// Gets tax rate
///
/// Product
/// Tax category identifier
/// Customer
/// Price (taxable value)
///
/// A task that represents the asynchronous operation
/// The task result contains the calculated tax rate. A value indicating whether a request is taxable
///
protected virtual async Task<(decimal taxRate, bool isTaxable)> GetTaxRateAsync(Product product, int taxCategoryId,
Customer customer, decimal price)
{
var taxRate = decimal.Zero;
//active tax provider
var store = await _storeContext.GetCurrentStoreAsync();
var activeTaxProvider = await _taxPluginManager.LoadPrimaryPluginAsync(customer, store.Id);
if (activeTaxProvider == null)
return (taxRate, true);
//tax request
var taxRateRequest = await PrepareTaxRateRequestAsync(product, taxCategoryId, customer, price);
var isTaxable = !await IsTaxExemptAsync(product, taxRateRequest.Customer);
//tax exempt
//make EU VAT exempt validation (the European Union Value Added Tax)
if (isTaxable &&
_taxSettings.EuVatEnabled &&
await IsVatExemptAsync(taxRateRequest.Address, taxRateRequest.Customer))
//VAT is not chargeable
isTaxable = false;
//get tax rate
var taxRateResult = await activeTaxProvider.GetTaxRateAsync(taxRateRequest);
//tax rate is calculated, now consumers can adjust it
await _eventPublisher.PublishAsync(new TaxRateCalculatedEvent(taxRateResult));
if (taxRateResult.Success)
{
//ensure that tax is equal or greater than zero
if (taxRateResult.TaxRate < decimal.Zero)
taxRateResult.TaxRate = decimal.Zero;
taxRate = taxRateResult.TaxRate;
}
else if (_taxSettings.LogErrors)
foreach (var error in taxRateResult.Errors)
await _logger.ErrorAsync($"{activeTaxProvider.PluginDescriptor.FriendlyName} - {error}", null, customer);
return (taxRate, isTaxable);
}
///
/// Gets VAT Number status
///
/// Two letter ISO code of a country
/// VAT number
///
/// A task that represents the asynchronous operation
/// The task result contains the vAT Number status. Name (if received). Address (if received)
///
protected virtual async Task<(VatNumberStatus vatNumberStatus, string name, string address)> GetVatNumberStatusAsync(string twoLetterIsoCode, string vatNumber)
{
var name = string.Empty;
var address = string.Empty;
if (string.IsNullOrEmpty(twoLetterIsoCode))
return (VatNumberStatus.Empty, name, address);
if (string.IsNullOrEmpty(vatNumber))
return (VatNumberStatus.Empty, name, address);
if (_taxSettings.EuVatAssumeValid)
return (VatNumberStatus.Valid, name, address);
if (!_taxSettings.EuVatUseWebService)
return (VatNumberStatus.Unknown, name, address);
var rez = await DoVatCheckAsync(twoLetterIsoCode, vatNumber);
return (rez.vatNumberStatus, rez.name, rez.address);
}
///
/// Performs a basic check of a VAT number for validity
///
/// Two letter ISO code of a country
/// VAT number
///
/// A task that represents the asynchronous operation
/// The task result contains the vAT number status. Company name. Address. Exception
///
protected virtual async Task<(VatNumberStatus vatNumberStatus, string name, string address, Exception exception)> DoVatCheckAsync(string twoLetterIsoCode, string vatNumber)
{
vatNumber ??= string.Empty;
vatNumber = vatNumber.Trim().Replace(" ", string.Empty);
twoLetterIsoCode ??= string.Empty;
try
{
var (status, name, address) = await _checkVatService.CheckVatAsync(twoLetterIsoCode, vatNumber);
return (status, name, address, null);
}
catch (Exception ex)
{
return (VatNumberStatus.Unknown, string.Empty, string.Empty, ex);
}
}
///
/// Gets a value indicating whether EU VAT exempt (the European Union Value Added Tax)
///
/// Address
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the result
///
protected virtual async Task IsVatExemptAsync(Address address, Customer customer)
{
if (!_taxSettings.EuVatEnabled)
return false;
if (customer == null || address == null)
return false;
var country = await _countryService.GetCountryByIdAsync(address.CountryId ?? 0);
if (country == null)
return false;
if (!country.SubjectToVat)
// VAT not chargeable if shipping outside VAT zone
return true;
// VAT not chargeable if address, customer and config meet our VAT exemption requirements:
// returns true if this customer is VAT exempt because they are shipping within the EU but outside our shop country, they have supplied a validated VAT number, and the shop is configured to allow VAT exemption
var customerVatStatus = (VatNumberStatus)customer.VatNumberStatusId;
return country.Id != _taxSettings.EuVatShopCountryId &&
customerVatStatus == VatNumberStatus.Valid &&
_taxSettings.EuVatAllowVatExemption;
}
#endregion
#region Methods
#region Product price
///
/// Gets price
///
/// Product
/// Price
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetProductPriceAsync(Product product, decimal price)
{
var customer = await _workContext.GetCurrentCustomerAsync();
return await GetProductPriceAsync(product, price, customer);
}
///
/// Gets price
///
/// Product
/// Price
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetProductPriceAsync(Product product, decimal price,
Customer customer)
{
var includingTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
return await GetProductPriceAsync(product, price, includingTax, customer);
}
///
/// Gets price
///
/// Product
/// Price
/// A value indicating whether calculated price should include tax
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetProductPriceAsync(Product product, decimal price,
bool includingTax, Customer customer)
{
var priceIncludesTax = _taxSettings.PricesIncludeTax;
var taxCategoryId = 0;
return await GetProductPriceAsync(product, taxCategoryId, price, includingTax, customer, priceIncludesTax);
}
///
/// Gets price
///
/// Product
/// Tax category identifier
/// Price
/// A value indicating whether calculated price should include tax
/// Customer
/// A value indicating whether price already includes tax
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetProductPriceAsync(Product product, int taxCategoryId,
decimal price, bool includingTax, Customer customer,
bool priceIncludesTax)
{
var taxRate = decimal.Zero;
//no need to calculate tax rate if passed "price" is 0
if (price == decimal.Zero)
return (price, taxRate);
bool isTaxable;
(taxRate, isTaxable) = await GetTaxRateAsync(product, taxCategoryId, customer, price);
if (priceIncludesTax)
{
//"price" already includes tax
if (includingTax)
{
//we should calculate price WITH tax
if (!isTaxable)
{
//but our request is not taxable
//hence we should calculate price WITHOUT tax
price = CalculatePrice(price, taxRate, false);
}
}
else
{
//we should calculate price WITHOUT tax
price = CalculatePrice(price, taxRate, false);
}
}
else
{
//"price" doesn't include tax
if (includingTax)
{
//we should calculate price WITH tax
//do it only when price is taxable
if (isTaxable)
{
price = CalculatePrice(price, taxRate, true);
}
}
}
if (!isTaxable)
{
//we return 0% tax rate in case a request is not taxable
taxRate = decimal.Zero;
}
//allowed to support negative price adjustments
//if (price < decimal.Zero)
// price = decimal.Zero;
return (price, taxRate);
}
///
/// Gets a value indicating whether a product is tax exempt
///
/// Product
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether a product is tax exempt
///
public virtual async Task IsTaxExemptAsync(Product product, Customer customer)
{
if (customer != null)
{
if (customer.IsTaxExempt)
return true;
if ((await _customerService.GetCustomerRolesAsync(customer)).Any(cr => cr.TaxExempt))
return true;
}
if (product == null)
return false;
if (product.IsTaxExempt)
return true;
return false;
}
#endregion
#region Shipping price
///
/// Gets shipping price
///
/// Price
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetShippingPriceAsync(decimal price, Customer customer)
{
var includingTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
return await GetShippingPriceAsync(price, includingTax, customer);
}
///
/// Gets shipping price
///
/// Price
/// A value indicating whether calculated price should include tax
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetShippingPriceAsync(decimal price, bool includingTax, Customer customer)
{
var taxRate = decimal.Zero;
if (!_taxSettings.ShippingIsTaxable)
{
return (price, taxRate);
}
var taxClassId = _taxSettings.ShippingTaxClassId;
var priceIncludesTax = _taxSettings.ShippingPriceIncludesTax;
return await GetProductPriceAsync(null, taxClassId, price, includingTax, customer, priceIncludesTax);
}
#endregion
#region Payment additional fee
///
/// Gets payment method additional handling fee
///
/// Price
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetPaymentMethodAdditionalFeeAsync(decimal price, Customer customer)
{
var includingTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
return await GetPaymentMethodAdditionalFeeAsync(price, includingTax, customer);
}
///
/// Gets payment method additional handling fee
///
/// Price
/// A value indicating whether calculated price should include tax
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetPaymentMethodAdditionalFeeAsync(decimal price, bool includingTax, Customer customer)
{
var taxRate = decimal.Zero;
if (!_taxSettings.PaymentMethodAdditionalFeeIsTaxable)
{
return (price, taxRate);
}
var taxClassId = _taxSettings.PaymentMethodAdditionalFeeTaxClassId;
var priceIncludesTax = _taxSettings.PaymentMethodAdditionalFeeIncludesTax;
return await GetProductPriceAsync(null, taxClassId, price, includingTax, customer, priceIncludesTax);
}
#endregion
#region Checkout attribute price
///
/// Gets checkout attribute value price
///
/// Checkout attribute
/// Checkout attribute value
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetCheckoutAttributePriceAsync(CheckoutAttribute ca, CheckoutAttributeValue cav)
{
var customer = await _workContext.GetCurrentCustomerAsync();
return await GetCheckoutAttributePriceAsync(ca, cav, customer);
}
///
/// Gets checkout attribute value price
///
/// Checkout attribute
/// Checkout attribute value
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetCheckoutAttributePriceAsync(CheckoutAttribute ca, CheckoutAttributeValue cav, Customer customer)
{
var includingTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
return await GetCheckoutAttributePriceAsync(ca, cav, includingTax, customer);
}
///
/// Gets checkout attribute value price
///
/// Checkout attribute
/// Checkout attribute value
/// A value indicating whether calculated price should include tax
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the price. Tax rate
///
public virtual async Task<(decimal price, decimal taxRate)> GetCheckoutAttributePriceAsync(CheckoutAttribute ca, CheckoutAttributeValue cav,
bool includingTax, Customer customer)
{
ArgumentNullException.ThrowIfNull(cav);
var taxRate = decimal.Zero;
var price = cav.PriceAdjustment;
if (ca.IsTaxExempt)
return (price, taxRate);
var priceIncludesTax = _taxSettings.PricesIncludeTax;
var taxClassId = ca.TaxCategoryId;
return await GetProductPriceAsync(null, taxClassId, price, includingTax, customer, priceIncludesTax);
}
#endregion
#region VAT
///
/// Gets VAT Number status
///
/// Two letter ISO code of a country and VAT number (e.g. DE 111 1111 111)
///
/// A task that represents the asynchronous operation
/// The task result contains the vAT Number status. Name (if received). Address (if received)
///
public virtual async Task<(VatNumberStatus vatNumberStatus, string name, string address)> GetVatNumberStatusAsync(string fullVatNumber)
{
var name = string.Empty;
var address = string.Empty;
if (string.IsNullOrWhiteSpace(fullVatNumber))
return (VatNumberStatus.Empty, name, address);
fullVatNumber = fullVatNumber.Trim();
//DE 111 1111 111 or DE 1111111111
var r = new Regex(@"^(\w{2})(.*)");
var match = r.Match(fullVatNumber);
if (!match.Success)
return (VatNumberStatus.Invalid, name, address);
var twoLetterIsoCode = match.Groups[1].Value;
var vatNumber = match.Groups[2].Value;
return await GetVatNumberStatusAsync(twoLetterIsoCode, vatNumber);
}
#endregion
#region Tax total
///
/// Get tax total for the passed shopping cart
///
/// Shopping cart
/// A value indicating whether we should use payment method additional fee when calculating tax
///
/// A task that represents the asynchronous operation
/// The task result contains the result
///
public virtual async Task GetTaxTotalAsync(IList cart, bool usePaymentMethodAdditionalFee = true)
{
var customer = await _customerService.GetShoppingCartCustomerAsync(cart);
var store = await _storeContext.GetCurrentStoreAsync();
var activeTaxProvider = await _taxPluginManager.LoadPrimaryPluginAsync(customer, store.Id);
if (activeTaxProvider == null)
return null;
//get result by using primary tax provider
var taxTotalRequest = new TaxTotalRequest
{
ShoppingCart = cart,
Customer = customer,
StoreId = store.Id,
UsePaymentMethodAdditionalFee = usePaymentMethodAdditionalFee
};
var taxTotalResult = await activeTaxProvider.GetTaxTotalAsync(taxTotalRequest);
//tax total is calculated, now consumers can adjust it
await _eventPublisher.PublishAsync(new TaxTotalCalculatedEvent(taxTotalRequest, taxTotalResult));
//error logging
if (taxTotalResult != null && !taxTotalResult.Success && _taxSettings.LogErrors)
{
foreach (var error in taxTotalResult.Errors)
{
await _logger.ErrorAsync($"{activeTaxProvider.PluginDescriptor.FriendlyName} - {error}", null, customer);
}
}
return taxTotalResult;
}
#endregion
#endregion
}