770 lines
28 KiB
C#
770 lines
28 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Tax service
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets a default tax address
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the address
|
|
/// </returns>
|
|
protected virtual async Task<Address> LoadDefaultTaxAddressAsync()
|
|
{
|
|
var addressId = _taxSettings.DefaultTaxAddressId;
|
|
|
|
return await _addressService.GetAddressByIdAsync(addressId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a pickup point address for tax calculation
|
|
/// </summary>
|
|
/// <param name="pickupPoint">Pickup point</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the address
|
|
/// </returns>
|
|
protected virtual async Task<Address> 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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepare request to get tax rate
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="taxCategoryId">Tax category identifier</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="price">Price</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the package for tax calculation
|
|
/// </returns>
|
|
protected virtual async Task<TaxRateRequest> 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<PickupPoint>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculated price
|
|
/// </summary>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="percent">Percent</param>
|
|
/// <param name="increase">Increase</param>
|
|
/// <returns>New price</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets tax rate
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="taxCategoryId">Tax category identifier</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="price">Price (taxable value)</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the calculated tax rate. A value indicating whether a request is taxable
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets VAT Number status
|
|
/// </summary>
|
|
/// <param name="twoLetterIsoCode">Two letter ISO code of a country</param>
|
|
/// <param name="vatNumber">VAT number</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the vAT Number status. Name (if received). Address (if received)
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs a basic check of a VAT number for validity
|
|
/// </summary>
|
|
/// <param name="twoLetterIsoCode">Two letter ISO code of a country</param>
|
|
/// <param name="vatNumber">VAT number</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the vAT number status. Company name. Address. Exception
|
|
/// </returns>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether EU VAT exempt (the European Union Value Added Tax)
|
|
/// </summary>
|
|
/// <param name="address">Address</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the result
|
|
/// </returns>
|
|
protected virtual async Task<bool> 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
|
|
|
|
/// <summary>
|
|
/// Gets price
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="price">Price</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
public virtual async Task<(decimal price, decimal taxRate)> GetProductPriceAsync(Product product, decimal price)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
|
|
return await GetProductPriceAsync(product, price, customer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets price
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets price
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="includingTax">A value indicating whether calculated price should include tax</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets price
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="taxCategoryId">Tax category identifier</param>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="includingTax">A value indicating whether calculated price should include tax</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="priceIncludesTax">A value indicating whether price already includes tax</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether a product is tax exempt
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains a value indicating whether a product is tax exempt
|
|
/// </returns>
|
|
public virtual async Task<bool> 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
|
|
|
|
/// <summary>
|
|
/// Gets shipping price
|
|
/// </summary>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets shipping price
|
|
/// </summary>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="includingTax">A value indicating whether calculated price should include tax</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets payment method additional handling fee
|
|
/// </summary>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets payment method additional handling fee
|
|
/// </summary>
|
|
/// <param name="price">Price</param>
|
|
/// <param name="includingTax">A value indicating whether calculated price should include tax</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets checkout attribute value price
|
|
/// </summary>
|
|
/// <param name="ca">Checkout attribute</param>
|
|
/// <param name="cav">Checkout attribute value</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
public virtual async Task<(decimal price, decimal taxRate)> GetCheckoutAttributePriceAsync(CheckoutAttribute ca, CheckoutAttributeValue cav)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
|
|
return await GetCheckoutAttributePriceAsync(ca, cav, customer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets checkout attribute value price
|
|
/// </summary>
|
|
/// <param name="ca">Checkout attribute</param>
|
|
/// <param name="cav">Checkout attribute value</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets checkout attribute value price
|
|
/// </summary>
|
|
/// <param name="ca">Checkout attribute</param>
|
|
/// <param name="cav">Checkout attribute value</param>
|
|
/// <param name="includingTax">A value indicating whether calculated price should include tax</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the price. Tax rate
|
|
/// </returns>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets VAT Number status
|
|
/// </summary>
|
|
/// <param name="fullVatNumber">Two letter ISO code of a country and VAT number (e.g. DE 111 1111 111)</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the vAT Number status. Name (if received). Address (if received)
|
|
/// </returns>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Get tax total for the passed shopping cart
|
|
/// </summary>
|
|
/// <param name="cart">Shopping cart</param>
|
|
/// <param name="usePaymentMethodAdditionalFee">A value indicating whether we should use payment method additional fee when calculating tax</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the result
|
|
/// </returns>
|
|
public virtual async Task<TaxTotalResult> GetTaxTotalAsync(IList<ShoppingCartItem> 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
|
|
} |