335 lines
15 KiB
C#
335 lines
15 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Nop.Core;
|
|
using Nop.Core.Caching;
|
|
using Nop.Core.Domain.Customers;
|
|
using Nop.Core.Domain.Tax;
|
|
using Nop.Plugin.Tax.FixedOrByCountryStateZip.Domain;
|
|
using Nop.Plugin.Tax.FixedOrByCountryStateZip.Infrastructure.Cache;
|
|
using Nop.Plugin.Tax.FixedOrByCountryStateZip.Services;
|
|
using Nop.Services.Common;
|
|
using Nop.Services.Configuration;
|
|
using Nop.Services.Localization;
|
|
using Nop.Services.Orders;
|
|
using Nop.Services.Payments;
|
|
using Nop.Services.Plugins;
|
|
using Nop.Services.Tax;
|
|
|
|
namespace Nop.Plugin.Tax.FixedOrByCountryStateZip;
|
|
|
|
/// <summary>
|
|
/// Fixed or by country & state & zip rate tax provider
|
|
/// </summary>
|
|
public class FixedOrByCountryStateZipTaxProvider : BasePlugin, ITaxProvider
|
|
{
|
|
#region Fields
|
|
|
|
protected readonly FixedOrByCountryStateZipTaxSettings _countryStateZipSettings;
|
|
protected readonly ICountryStateZipService _taxRateService;
|
|
protected readonly IGenericAttributeService _genericAttributeService;
|
|
protected readonly IHttpContextAccessor _httpContextAccessor;
|
|
protected readonly ILocalizationService _localizationService;
|
|
protected readonly IOrderTotalCalculationService _orderTotalCalculationService;
|
|
protected readonly IPaymentService _paymentService;
|
|
protected readonly ISettingService _settingService;
|
|
protected readonly IStaticCacheManager _staticCacheManager;
|
|
protected readonly ITaxCategoryService _taxCategoryService;
|
|
protected readonly ITaxService _taxService;
|
|
protected readonly IWebHelper _webHelper;
|
|
protected readonly TaxSettings _taxSettings;
|
|
|
|
#endregion
|
|
|
|
#region Ctor
|
|
|
|
public FixedOrByCountryStateZipTaxProvider(FixedOrByCountryStateZipTaxSettings countryStateZipSettings,
|
|
ICountryStateZipService taxRateService,
|
|
IGenericAttributeService genericAttributeService,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
ILocalizationService localizationService,
|
|
IOrderTotalCalculationService orderTotalCalculationService,
|
|
IPaymentService paymentService,
|
|
ISettingService settingService,
|
|
IStaticCacheManager staticCacheManager,
|
|
ITaxCategoryService taxCategoryService,
|
|
ITaxService taxService,
|
|
IWebHelper webHelper,
|
|
TaxSettings taxSettings)
|
|
{
|
|
_countryStateZipSettings = countryStateZipSettings;
|
|
_taxRateService = taxRateService;
|
|
_genericAttributeService = genericAttributeService;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
_localizationService = localizationService;
|
|
_orderTotalCalculationService = orderTotalCalculationService;
|
|
_paymentService = paymentService;
|
|
_settingService = settingService;
|
|
_staticCacheManager = staticCacheManager;
|
|
_taxCategoryService = taxCategoryService;
|
|
_taxService = taxService;
|
|
_webHelper = webHelper;
|
|
_taxSettings = taxSettings;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Gets tax rate
|
|
/// </summary>
|
|
/// <param name="taxRateRequest">Tax rate request</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the ax
|
|
/// </returns>
|
|
public async Task<TaxRateResult> GetTaxRateAsync(TaxRateRequest taxRateRequest)
|
|
{
|
|
var result = new TaxRateResult();
|
|
|
|
//the tax rate calculation by fixed rate
|
|
if (!_countryStateZipSettings.CountryStateZipEnabled)
|
|
{
|
|
result.TaxRate = await _settingService.GetSettingByKeyAsync<decimal>(string.Format(FixedOrByCountryStateZipDefaults.FIXED_RATE_SETTINGS_KEY, taxRateRequest.TaxCategoryId));
|
|
return result;
|
|
}
|
|
|
|
//the tax rate calculation by country & state & zip
|
|
if (taxRateRequest.Address == null)
|
|
{
|
|
result.Errors.Add("Address is not set");
|
|
return result;
|
|
}
|
|
|
|
//first, load all tax rate records (cached) - loaded only once
|
|
var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(ModelCacheEventConsumer.ALL_TAX_RATES_MODEL_KEY);
|
|
var allTaxRates = await _staticCacheManager.GetAsync(cacheKey, async () => (await _taxRateService.GetAllTaxRatesAsync()).Select(taxRate => new TaxRate
|
|
{
|
|
Id = taxRate.Id,
|
|
StoreId = taxRate.StoreId,
|
|
TaxCategoryId = taxRate.TaxCategoryId,
|
|
CountryId = taxRate.CountryId,
|
|
StateProvinceId = taxRate.StateProvinceId,
|
|
Zip = taxRate.Zip,
|
|
Percentage = taxRate.Percentage
|
|
}).ToList());
|
|
|
|
var storeId = taxRateRequest.CurrentStoreId;
|
|
var taxCategoryId = taxRateRequest.TaxCategoryId;
|
|
var countryId = taxRateRequest.Address.CountryId;
|
|
var stateProvinceId = taxRateRequest.Address.StateProvinceId;
|
|
var zip = taxRateRequest.Address.ZipPostalCode?.Trim() ?? string.Empty;
|
|
|
|
var existingRates = allTaxRates.Where(taxRate => taxRate.CountryId == countryId && taxRate.TaxCategoryId == taxCategoryId);
|
|
|
|
//filter by store
|
|
var matchedByStore = existingRates.Where(taxRate => storeId == taxRate.StoreId || taxRate.StoreId == 0);
|
|
|
|
//filter by state/province
|
|
var matchedByStateProvince = matchedByStore.Where(taxRate => stateProvinceId == taxRate.StateProvinceId || taxRate.StateProvinceId == 0);
|
|
|
|
//filter by zip
|
|
var matchedByZip = matchedByStateProvince.Where(taxRate => string.IsNullOrWhiteSpace(taxRate.Zip) || taxRate.Zip.Equals(zip, StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
//sort from particular to general, more particular cases will be the first
|
|
var foundRecords = matchedByZip.OrderBy(r => r.StoreId == 0).ThenBy(r => r.StateProvinceId == 0).ThenBy(r => string.IsNullOrEmpty(r.Zip));
|
|
|
|
var foundRecord = foundRecords.FirstOrDefault();
|
|
|
|
if (foundRecord != null)
|
|
result.TaxRate = foundRecord.Percentage;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets tax total
|
|
/// </summary>
|
|
/// <param name="taxTotalRequest">Tax total request</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the ax total
|
|
/// </returns>
|
|
public async Task<TaxTotalResult> GetTaxTotalAsync(TaxTotalRequest taxTotalRequest)
|
|
{
|
|
if (_httpContextAccessor.HttpContext.Items.TryGetValue("nop.TaxTotal", out var result)
|
|
&& result is (TaxTotalResult taxTotalResult, decimal paymentTax))
|
|
{
|
|
//short-circuit to avoid circular reference when calculating payment method additional fee during the checkout process
|
|
if (!taxTotalRequest.UsePaymentMethodAdditionalFee)
|
|
return new TaxTotalResult { TaxTotal = taxTotalResult.TaxTotal - paymentTax };
|
|
|
|
return taxTotalResult;
|
|
}
|
|
|
|
var taxRates = new SortedDictionary<decimal, decimal>();
|
|
var taxTotal = decimal.Zero;
|
|
|
|
//order sub total (items + checkout attributes)
|
|
var (_, _, _, _, orderSubTotalTaxRates) = await _orderTotalCalculationService
|
|
.GetShoppingCartSubTotalAsync(taxTotalRequest.ShoppingCart, false);
|
|
var subTotalTaxTotal = decimal.Zero;
|
|
foreach (var kvp in orderSubTotalTaxRates)
|
|
{
|
|
var taxRate = kvp.Key;
|
|
var taxValue = kvp.Value;
|
|
subTotalTaxTotal += taxValue;
|
|
|
|
if (taxRate > decimal.Zero && taxValue > decimal.Zero)
|
|
{
|
|
if (!taxRates.TryGetValue(taxRate, out var value))
|
|
taxRates.Add(taxRate, taxValue);
|
|
else
|
|
taxRates[taxRate] = value + taxValue;
|
|
}
|
|
}
|
|
taxTotal += subTotalTaxTotal;
|
|
|
|
//shipping
|
|
var shippingTax = decimal.Zero;
|
|
if (_taxSettings.ShippingIsTaxable)
|
|
{
|
|
var (shippingExclTax, _, _) = await _orderTotalCalculationService
|
|
.GetShoppingCartShippingTotalAsync(taxTotalRequest.ShoppingCart, false);
|
|
var (shippingInclTax, taxRate, _) = await _orderTotalCalculationService
|
|
.GetShoppingCartShippingTotalAsync(taxTotalRequest.ShoppingCart, true);
|
|
if (shippingExclTax.HasValue && shippingInclTax.HasValue)
|
|
{
|
|
shippingTax = shippingInclTax.Value - shippingExclTax.Value;
|
|
if (shippingTax < decimal.Zero)
|
|
shippingTax = decimal.Zero;
|
|
|
|
if (taxRate > decimal.Zero && shippingTax > decimal.Zero)
|
|
{
|
|
if (!taxRates.TryGetValue(taxRate, out var value))
|
|
taxRates.Add(taxRate, shippingTax);
|
|
else
|
|
taxRates[taxRate] = value + shippingTax;
|
|
}
|
|
}
|
|
}
|
|
taxTotal += shippingTax;
|
|
|
|
//short-circuit to avoid circular reference when calculating payment method additional fee during the checkout process
|
|
if (!taxTotalRequest.UsePaymentMethodAdditionalFee)
|
|
return new TaxTotalResult { TaxTotal = taxTotal };
|
|
|
|
//payment method additional fee
|
|
var paymentMethodAdditionalFeeTax = decimal.Zero;
|
|
if (_taxSettings.PaymentMethodAdditionalFeeIsTaxable)
|
|
{
|
|
var paymentMethodSystemName = taxTotalRequest.Customer != null
|
|
? await _genericAttributeService
|
|
.GetAttributeAsync<string>(taxTotalRequest.Customer, NopCustomerDefaults.SelectedPaymentMethodAttribute, taxTotalRequest.StoreId)
|
|
: string.Empty;
|
|
|
|
var paymentMethodAdditionalFee = await _paymentService
|
|
.GetAdditionalHandlingFeeAsync(taxTotalRequest.ShoppingCart, paymentMethodSystemName);
|
|
var (paymentMethodAdditionalFeeExclTax, _) = await _taxService
|
|
.GetPaymentMethodAdditionalFeeAsync(paymentMethodAdditionalFee, false, taxTotalRequest.Customer);
|
|
var (paymentMethodAdditionalFeeInclTax, taxRate) = await _taxService
|
|
.GetPaymentMethodAdditionalFeeAsync(paymentMethodAdditionalFee, true, taxTotalRequest.Customer);
|
|
|
|
paymentMethodAdditionalFeeTax = paymentMethodAdditionalFeeInclTax - paymentMethodAdditionalFeeExclTax;
|
|
if (paymentMethodAdditionalFeeTax < decimal.Zero)
|
|
paymentMethodAdditionalFeeTax = decimal.Zero;
|
|
|
|
if (taxRate > decimal.Zero && paymentMethodAdditionalFeeTax > decimal.Zero)
|
|
{
|
|
if (!taxRates.TryGetValue(taxRate, out var value))
|
|
taxRates.Add(taxRate, paymentMethodAdditionalFeeTax);
|
|
else
|
|
taxRates[taxRate] = value + paymentMethodAdditionalFeeTax;
|
|
}
|
|
}
|
|
taxTotal += paymentMethodAdditionalFeeTax;
|
|
|
|
//add at least one tax rate (0%)
|
|
if (!taxRates.Any())
|
|
taxRates.Add(decimal.Zero, decimal.Zero);
|
|
|
|
if (taxTotal < decimal.Zero)
|
|
taxTotal = decimal.Zero;
|
|
|
|
taxTotalResult = new TaxTotalResult { TaxTotal = taxTotal, TaxRates = taxRates, };
|
|
|
|
//store values within the scope of the request to avoid duplicate calculations
|
|
_httpContextAccessor.HttpContext.Items.TryAdd("nop.TaxTotal", (taxTotalResult, paymentMethodAdditionalFeeTax));
|
|
|
|
return taxTotalResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a configuration page URL
|
|
/// </summary>
|
|
public override string GetConfigurationPageUrl()
|
|
{
|
|
return $"{_webHelper.GetStoreLocation()}Admin/FixedOrByCountryStateZip/Configure";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Install plugin
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public override async Task InstallAsync()
|
|
{
|
|
//settings
|
|
await _settingService.SaveSettingAsync(new FixedOrByCountryStateZipTaxSettings());
|
|
|
|
//locales
|
|
await _localizationService.AddOrUpdateLocaleResourceAsync(new Dictionary<string, string>
|
|
{
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fixed"] = "Fixed rate",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Tax.Categories.Manage"] = "Manage tax categories",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.TaxCategoriesCanNotLoaded"] = "No tax categories can be loaded. You may manage tax categories by <a href='{0}'>this link</a>",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.TaxByCountryStateZip"] = "By Country",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.TaxCategoryName"] = "Tax category",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Rate"] = "Rate",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Store"] = "Store",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Store.Hint"] = "If an asterisk is selected, then this shipping rate will apply to all stores.",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Country"] = "Country",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Country.Hint"] = "The country.",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.StateProvince"] = "State / province",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.StateProvince.Hint"] = "If an asterisk is selected, then this tax rate will apply to all customers from the given country, regardless of the state.",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Zip"] = "Zip",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Zip.Hint"] = "Zip / postal code. If zip is empty, then this tax rate will apply to all customers from the given country or state, regardless of the zip code.",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.TaxCategory"] = "Tax category",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.TaxCategory.Hint"] = "The tax category.",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Percentage"] = "Percentage",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.Fields.Percentage.Hint"] = "The tax rate.",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.AddRecord"] = "Add tax rate",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.AddRecordTitle"] = "New tax rate",
|
|
["Plugins.Tax.FixedOrByCountryStateZip.SwitchRate"] = @"
|
|
<p>
|
|
You are going to change the way the tax rate is calculated. This will cause the tax rate to be calculated based on the settings specified on the configuration page.
|
|
</p>
|
|
<p>
|
|
Any current tax rate settings will be saved, but will not be active until you return to this tax calculation method.
|
|
</p>",
|
|
});
|
|
|
|
await base.InstallAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uninstall plugin
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public override async Task UninstallAsync()
|
|
{
|
|
//settings
|
|
await _settingService.DeleteSettingAsync<FixedOrByCountryStateZipTaxSettings>();
|
|
|
|
//fixed rates
|
|
var fixedRates = await (await _taxCategoryService.GetAllTaxCategoriesAsync())
|
|
.SelectAwait(async taxCategory => await _settingService.GetSettingAsync(string.Format(FixedOrByCountryStateZipDefaults.FIXED_RATE_SETTINGS_KEY, taxCategory.Id)))
|
|
.Where(setting => setting != null).ToListAsync();
|
|
await _settingService.DeleteSettingsAsync(fixedRates);
|
|
|
|
//locales
|
|
await _localizationService.DeleteLocaleResourcesAsync("Plugins.Tax.FixedOrByCountryStateZip");
|
|
|
|
await base.UninstallAsync();
|
|
}
|
|
|
|
#endregion
|
|
} |