Try your search with a different keyword or use * as a wildcard.
using Nop.Core.Caching;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Stores;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Discounts;
namespace Nop.Services.Catalog;
/// <summary>
/// Price calculation service
/// </summary>
public partial class PriceCalculationService : IPriceCalculationService
{
#region Fields
protected readonly CatalogSettings _catalogSettings;
protected readonly CurrencySettings _currencySettings;
protected readonly ICategoryService _categoryService;
protected readonly ICurrencyService _currencyService;
protected readonly ICustomerService _customerService;
protected readonly IDiscountService _discountService;
protected readonly IManufacturerService _manufacturerService;
protected readonly IProductAttributeParser _productAttributeParser;
protected readonly IProductService _productService;
protected readonly IStaticCacheManager _staticCacheManager;
#endregion
#region Ctor
public PriceCalculationService(CatalogSettings catalogSettings,
CurrencySettings currencySettings,
ICategoryService categoryService,
ICurrencyService currencyService,
ICustomerService customerService,
IDiscountService discountService,
IManufacturerService manufacturerService,
IProductAttributeParser productAttributeParser,
IProductService productService,
IStaticCacheManager staticCacheManager)
{
_catalogSettings = catalogSettings;
_currencySettings = currencySettings;
_categoryService = categoryService;
_currencyService = currencyService;
_customerService = customerService;
_discountService = discountService;
_manufacturerService = manufacturerService;
_productAttributeParser = productAttributeParser;
_productService = productService;
_staticCacheManager = staticCacheManager;
}
#endregion
#region Utilities
/// <summary>
/// Gets allowed discounts applied to product
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">Customer</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the discounts
/// </returns>
protected virtual async Task<IList<Discount>> GetAllowedDiscountsAppliedToProductAsync(Product product, Customer customer)
{
var allowedDiscounts = new List<Discount>();
if (_catalogSettings.IgnoreDiscounts)
return allowedDiscounts;
var couponCodesToValidate = await _customerService.ParseAppliedDiscountCouponCodesAsync(customer);
foreach (var discount in await _discountService.GetAppliedDiscountsAsync(product))
if (discount.DiscountType == DiscountType.AssignedToSkus &&
(await _discountService.ValidateDiscountAsync(discount, customer, couponCodesToValidate)).IsValid)
allowedDiscounts.Add(discount);
return allowedDiscounts;
}
/// <summary>
/// Gets allowed discounts applied to categories
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">Customer</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the discounts
/// </returns>
protected virtual async Task<IList<Discount>> GetAllowedDiscountsAppliedToCategoriesAsync(Product product, Customer customer)
{
var allowedDiscounts = new List<Discount>();
if (_catalogSettings.IgnoreDiscounts)
return allowedDiscounts;
//load cached discount models (performance optimization)
foreach (var discount in await _discountService.GetAllDiscountsAsync(DiscountType.AssignedToCategories))
{
//load identifier of categories with this discount applied to
var discountCategoryIds = await _categoryService.GetAppliedCategoryIdsAsync(discount, customer);
//compare with categories of this product
var productCategoryIds = new List<int>();
if (discountCategoryIds.Any())
{
productCategoryIds = (await _categoryService
.GetProductCategoriesByProductIdAsync(product.Id))
.Select(x => x.CategoryId)
.ToList();
}
var couponCodesToValidate = await _customerService.ParseAppliedDiscountCouponCodesAsync(customer);
foreach (var categoryId in productCategoryIds)
{
if (!discountCategoryIds.Contains(categoryId))
continue;
if (!_discountService.ContainsDiscount(allowedDiscounts, discount) &&
(await _discountService.ValidateDiscountAsync(discount, customer, couponCodesToValidate)).IsValid)
allowedDiscounts.Add(discount);
}
}
return allowedDiscounts;
}
/// <summary>
/// Gets allowed discounts applied to manufacturers
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">Customer</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the discounts
/// </returns>
protected virtual async Task<IList<Discount>> GetAllowedDiscountsAppliedToManufacturersAsync(Product product, Customer customer)
{
var allowedDiscounts = new List<Discount>();
if (_catalogSettings.IgnoreDiscounts)
return allowedDiscounts;
foreach (var discount in await _discountService.GetAllDiscountsAsync(DiscountType.AssignedToManufacturers))
{
//load identifier of manufacturers with this discount applied to
var discountManufacturerIds = await _manufacturerService.GetAppliedManufacturerIdsAsync(discount, customer);
//compare with manufacturers of this product
var productManufacturerIds = new List<int>();
if (discountManufacturerIds.Any())
{
productManufacturerIds =
(await _manufacturerService
.GetProductManufacturersByProductIdAsync(product.Id))
.Select(x => x.ManufacturerId)
.ToList();
}
var couponCodesToValidate = await _customerService.ParseAppliedDiscountCouponCodesAsync(customer);
foreach (var manufacturerId in productManufacturerIds)
{
if (!discountManufacturerIds.Contains(manufacturerId))
continue;
if (!_discountService.ContainsDiscount(allowedDiscounts, discount) &&
(await _discountService.ValidateDiscountAsync(discount, customer, couponCodesToValidate)).IsValid)
allowedDiscounts.Add(discount);
}
}
return allowedDiscounts;
}
/// <summary>
/// Gets allowed discounts
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">Customer</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the discounts
/// </returns>
protected virtual async Task<IList<Discount>> GetAllowedDiscountsAsync(Product product, Customer customer)
{
var allowedDiscounts = new List<Discount>();
if (_catalogSettings.IgnoreDiscounts)
return allowedDiscounts;
//discounts applied to products
foreach (var discount in await GetAllowedDiscountsAppliedToProductAsync(product, customer))
if (!_discountService.ContainsDiscount(allowedDiscounts, discount))
allowedDiscounts.Add(discount);
//discounts applied to categories
foreach (var discount in await GetAllowedDiscountsAppliedToCategoriesAsync(product, customer))
if (!_discountService.ContainsDiscount(allowedDiscounts, discount))
allowedDiscounts.Add(discount);
//discounts applied to manufacturers
foreach (var discount in await GetAllowedDiscountsAppliedToManufacturersAsync(product, customer))
if (!_discountService.ContainsDiscount(allowedDiscounts, discount))
allowedDiscounts.Add(discount);
return allowedDiscounts;
}
/// <summary>
/// Gets discount amount
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">The customer</param>
/// <param name="productPriceWithoutDiscount">Already calculated product price without discount</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the discount amount, Applied discounts
/// </returns>
protected virtual async Task<(decimal, List<Discount>)> GetDiscountAmountAsync(Product product,
Customer customer,
decimal productPriceWithoutDiscount)
{
ArgumentNullException.ThrowIfNull(product);
var appliedDiscounts = new List<Discount>();
var appliedDiscountAmount = decimal.Zero;
//we don't apply discounts to products with price entered by a customer
if (product.CustomerEntersPrice)
return (appliedDiscountAmount, appliedDiscounts);
//discounts are disabled
if (_catalogSettings.IgnoreDiscounts)
return (appliedDiscountAmount, appliedDiscounts);
var allowedDiscounts = await GetAllowedDiscountsAsync(product, customer);
//no discounts
if (!allowedDiscounts.Any())
return (appliedDiscountAmount, appliedDiscounts);
appliedDiscounts = _discountService.GetPreferredDiscount(allowedDiscounts, productPriceWithoutDiscount, out appliedDiscountAmount);
return (appliedDiscountAmount, appliedDiscounts);
}
#endregion
#region Methods
/// <summary>
/// Gets the final price
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">The customer</param>
/// <param name="store">Store</param>
/// <param name="additionalCharge">Additional charge</param>
/// <param name="includeDiscounts">A value indicating whether include discounts or not for final price computation</param>
/// <param name="quantity">Shopping cart item quantity</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the final price without discounts, Final price, Applied discount amount, Applied discounts
/// </returns>
public virtual async Task<(decimal priceWithoutDiscounts, decimal finalPrice, decimal appliedDiscountAmount, List<Discount> appliedDiscounts)> GetFinalPriceAsync(Product product,
Customer customer,
Store store,
decimal additionalCharge = 0,
bool includeDiscounts = true,
int quantity = 1)
{
return await GetFinalPriceAsync(product, customer, store,
additionalCharge, includeDiscounts, quantity,
null, null);
}
/// <summary>
/// Gets the final price
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">The customer</param>
/// <param name="store">Store</param>
/// <param name="additionalCharge">Additional charge</param>
/// <param name="includeDiscounts">A value indicating whether include discounts or not for final price computation</param>
/// <param name="quantity">Shopping cart item quantity</param>
/// <param name="rentalStartDate">Rental period start date (for rental products)</param>
/// <param name="rentalEndDate">Rental period end date (for rental products)</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the final price without discounts, Final price, Applied discount amount, Applied discounts
/// </returns>
public virtual async Task<(decimal priceWithoutDiscounts, decimal finalPrice, decimal appliedDiscountAmount, List<Discount> appliedDiscounts)> GetFinalPriceAsync(Product product,
Customer customer,
Store store,
decimal additionalCharge,
bool includeDiscounts,
int quantity,
DateTime? rentalStartDate,
DateTime? rentalEndDate)
{
return await GetFinalPriceAsync(product, customer, store, null, additionalCharge, includeDiscounts, quantity,
rentalStartDate, rentalEndDate);
}
/// <summary>
/// Gets the final price
/// </summary>
/// <param name="product">Product</param>
/// <param name="customer">The customer</param>
/// <param name="store">Store</param>
/// <param name="overriddenProductPrice">Overridden product price. If specified, then it'll be used instead of a product price. For example, used with product attribute combinations</param>
/// <param name="additionalCharge">Additional charge</param>
/// <param name="includeDiscounts">A value indicating whether include discounts or not for final price computation</param>
/// <param name="quantity">Shopping cart item quantity</param>
/// <param name="rentalStartDate">Rental period start date (for rental products)</param>
/// <param name="rentalEndDate">Rental period end date (for rental products)</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the final price without discounts, Final price, Applied discount amount, Applied discounts
/// </returns>
public virtual async Task<(decimal priceWithoutDiscounts, decimal finalPrice, decimal appliedDiscountAmount, List<Discount> appliedDiscounts)> GetFinalPriceAsync(Product product,
Customer customer,
Store store,
decimal? overriddenProductPrice,
decimal additionalCharge,
bool includeDiscounts,
int quantity,
DateTime? rentalStartDate,
DateTime? rentalEndDate)
{
ArgumentNullException.ThrowIfNull(product);
var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.ProductPriceCacheKey,
product,
overriddenProductPrice,
additionalCharge,
includeDiscounts,
quantity,
await _customerService.GetCustomerRoleIdsAsync(customer),
store);
//we do not cache price if this not allowed by settings or if the product is rental product
//otherwise, it can cause memory leaks (to store all possible date period combinations)
if (!_catalogSettings.CacheProductPrices || product.IsRental)
cacheKey.CacheTime = 0;
decimal rezPrice;
decimal rezPriceWithoutDiscount;
decimal discountAmount;
List<Discount> appliedDiscounts;
(rezPriceWithoutDiscount, rezPrice, discountAmount, appliedDiscounts) = await _staticCacheManager.GetAsync(cacheKey, async () =>
{
var discounts = new List<Discount>();
var appliedDiscountAmount = decimal.Zero;
//initial price
var price = overriddenProductPrice ?? product.Price;
//tier prices
var tierPrice = await _productService.GetPreferredTierPriceAsync(product, customer, store, quantity);
if (tierPrice != null)
price = tierPrice.Price;
//additional charge
price += additionalCharge;
//rental products
if (product.IsRental)
if (rentalStartDate.HasValue && rentalEndDate.HasValue)
price *= _productService.GetRentalPeriods(product, rentalStartDate.Value, rentalEndDate.Value);
var priceWithoutDiscount = price;
if (includeDiscounts)
{
//discount
var (tmpDiscountAmount, tmpAppliedDiscounts) = await GetDiscountAmountAsync(product, customer, price);
price -= tmpDiscountAmount;
if (tmpAppliedDiscounts?.Any() ?? false)
{
discounts.AddRange(tmpAppliedDiscounts);
appliedDiscountAmount = tmpDiscountAmount;
}
}
if (price < decimal.Zero)
price = decimal.Zero;
if (priceWithoutDiscount < decimal.Zero)
priceWithoutDiscount = decimal.Zero;
return (priceWithoutDiscount, price, appliedDiscountAmount, discounts);
});
return (rezPriceWithoutDiscount, rezPrice, discountAmount, appliedDiscounts);
}
/// <summary>
/// Gets the product cost (one item)
/// </summary>
/// <param name="product">Product</param>
/// <param name="attributesXml">Shopping cart item attributes in XML</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the product cost (one item)
/// </returns>
public virtual async Task<decimal> GetProductCostAsync(Product product, string attributesXml)
{
ArgumentNullException.ThrowIfNull(product);
var cost = product.ProductCost;
var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(attributesXml);
foreach (var attributeValue in attributeValues)
{
switch (attributeValue.AttributeValueType)
{
case AttributeValueType.Simple:
//simple attribute
cost += attributeValue.Cost;
break;
case AttributeValueType.AssociatedToProduct:
//bundled product
var associatedProduct = await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId);
if (associatedProduct != null)
cost += associatedProduct.ProductCost * attributeValue.Quantity;
break;
default:
break;
}
}
return cost;
}
/// <summary>
/// Get a price adjustment of a product attribute value
/// </summary>
/// <param name="product">Product</param>
/// <param name="value">Product attribute value</param>
/// <param name="customer">Customer</param>
/// <param name="store">Store</param>
/// <param name="productPrice">Product price (null for using the base product price)</param>
/// <param name="quantity">Shopping cart item quantity</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the price adjustment
/// </returns>
public virtual async Task<decimal> GetProductAttributeValuePriceAdjustmentAsync(Product product,
ProductAttributeValue value,
Customer customer,
Store store,
decimal? productPrice = null,
int quantity = 1)
{
ArgumentNullException.ThrowIfNull(value);
var adjustment = decimal.Zero;
switch (value.AttributeValueType)
{
case AttributeValueType.Simple:
//simple attribute
if (value.PriceAdjustmentUsePercentage)
{
if (!productPrice.HasValue)
productPrice = (await GetFinalPriceAsync(product, customer, store, quantity: quantity)).finalPrice;
adjustment = (decimal)((float)productPrice * (float)value.PriceAdjustment / 100f);
}
else
{
adjustment = value.PriceAdjustment;
}
break;
case AttributeValueType.AssociatedToProduct:
//bundled product
var associatedProduct = await _productService.GetProductByIdAsync(value.AssociatedProductId);
if (associatedProduct != null)
adjustment = (await GetFinalPriceAsync(associatedProduct, customer, store)).finalPrice * value.Quantity;
break;
default:
break;
}
return adjustment;
}
/// <summary>
/// Round a product or order total for the currency
/// </summary>
/// <param name="value">Value to round</param>
/// <param name="currency">Currency; pass null to use the primary store currency</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the rounded value
/// </returns>
public virtual async Task<decimal> RoundPriceAsync(decimal value, Currency currency = null)
{
//we use this method because some currencies (e.g. Hungarian Forint or Swiss Franc) use non-standard rules for rounding
//you can implement any rounding logic here
currency ??= await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId);
return Round(value, currency.RoundingType);
}
/// <summary>
/// Round
/// </summary>
/// <param name="value">Value to round</param>
/// <param name="roundingType">The rounding type</param>
/// <returns>Rounded value</returns>
public virtual decimal Round(decimal value, RoundingType roundingType)
{
//default round (Rounding001)
var rez = Math.Round(value, 2);
var fractionPart = (rez - Math.Truncate(rez)) * 10;
//cash rounding not needed
if (fractionPart == 0)
return rez;
//Cash rounding (details: https://en.wikipedia.org/wiki/Cash_rounding)
switch (roundingType)
{
//rounding with 0.05 or 5 intervals
case RoundingType.Rounding005Up:
case RoundingType.Rounding005Down:
fractionPart = (fractionPart - Math.Truncate(fractionPart)) * 10;
fractionPart %= 5;
if (fractionPart == 0)
break;
if (roundingType == RoundingType.Rounding005Up)
fractionPart = 5 - fractionPart;
else
fractionPart *= -1;
rez += fractionPart / 100;
break;
//rounding with 0.10 intervals
case RoundingType.Rounding01Up:
case RoundingType.Rounding01Down:
fractionPart = (fractionPart - Math.Truncate(fractionPart)) * 10;
if (roundingType == RoundingType.Rounding01Down && fractionPart == 5)
fractionPart = -5;
else
fractionPart = fractionPart < 5 ? fractionPart * -1 : 10 - fractionPart;
rez += fractionPart / 100;
break;
//rounding with 0.50 intervals
case RoundingType.Rounding05:
fractionPart *= 10;
fractionPart = fractionPart < 25 ? fractionPart * -1 : fractionPart < 50 || fractionPart < 75 ? 50 - fractionPart : 100 - fractionPart;
rez += fractionPart / 100;
break;
//rounding with 1.00 intervals
case RoundingType.Rounding1:
case RoundingType.Rounding1Up:
fractionPart *= 10;
if (roundingType == RoundingType.Rounding1Up && fractionPart > 0)
rez = Math.Truncate(rez) + 1;
else
rez = fractionPart < 50 ? Math.Truncate(rez) : Math.Truncate(rez) + 1;
break;
case RoundingType.Rounding001:
default:
break;
}
return rez;
}
#endregion
}