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;
///
/// Price calculation service
///
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
///
/// Gets allowed discounts applied to product
///
/// Product
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the discounts
///
protected virtual async Task> GetAllowedDiscountsAppliedToProductAsync(Product product, Customer customer)
{
var allowedDiscounts = new List();
if (_catalogSettings.IgnoreDiscounts)
return allowedDiscounts;
if (!product.HasDiscountsApplied)
return allowedDiscounts;
var couponCodesToValidate = await _customerService.ParseAppliedDiscountCouponCodesAsync(customer);
//we use this property ("HasDiscountsApplied") for performance optimization to avoid unnecessary database calls
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;
}
///
/// Gets allowed discounts applied to categories
///
/// Product
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the discounts
///
protected virtual async Task> GetAllowedDiscountsAppliedToCategoriesAsync(Product product, Customer customer)
{
var allowedDiscounts = new List();
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();
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;
}
///
/// Gets allowed discounts applied to manufacturers
///
/// Product
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the discounts
///
protected virtual async Task> GetAllowedDiscountsAppliedToManufacturersAsync(Product product, Customer customer)
{
var allowedDiscounts = new List();
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();
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;
}
///
/// Gets allowed discounts
///
/// Product
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the discounts
///
protected virtual async Task> GetAllowedDiscountsAsync(Product product, Customer customer)
{
var allowedDiscounts = new List();
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;
}
///
/// Gets discount amount
///
/// Product
/// The customer
/// Already calculated product price without discount
///
/// A task that represents the asynchronous operation
/// The task result contains the discount amount, Applied discounts
///
protected virtual async Task<(decimal, List)> GetDiscountAmountAsync(Product product,
Customer customer,
decimal productPriceWithoutDiscount)
{
ArgumentNullException.ThrowIfNull(product);
var appliedDiscounts = new List();
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
///
/// Gets the final price
///
/// Product
/// The customer
/// Store
/// Additional charge
/// A value indicating whether include discounts or not for final price computation
/// Shopping cart item quantity
///
/// A task that represents the asynchronous operation
/// The task result contains the final price without discounts, Final price, Applied discount amount, Applied discounts
///
public virtual async Task<(decimal priceWithoutDiscounts, decimal finalPrice, decimal appliedDiscountAmount, List 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);
}
///
/// Gets the final price
///
/// Product
/// The customer
/// Store
/// Additional charge
/// A value indicating whether include discounts or not for final price computation
/// Shopping cart item quantity
/// Rental period start date (for rental products)
/// Rental period end date (for rental products)
///
/// A task that represents the asynchronous operation
/// The task result contains the final price without discounts, Final price, Applied discount amount, Applied discounts
///
public virtual async Task<(decimal priceWithoutDiscounts, decimal finalPrice, decimal appliedDiscountAmount, List 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);
}
///
/// Gets the final price
///
/// Product
/// The customer
/// Store
/// Overridden product price. If specified, then it'll be used instead of a product price. For example, used with product attribute combinations
/// Additional charge
/// A value indicating whether include discounts or not for final price computation
/// Shopping cart item quantity
/// Rental period start date (for rental products)
/// Rental period end date (for rental products)
///
/// A task that represents the asynchronous operation
/// The task result contains the final price without discounts, Final price, Applied discount amount, Applied discounts
///
public virtual async Task<(decimal priceWithoutDiscounts, decimal finalPrice, decimal appliedDiscountAmount, List 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 appliedDiscounts;
(rezPriceWithoutDiscount, rezPrice, discountAmount, appliedDiscounts) = await _staticCacheManager.GetAsync(cacheKey, async () =>
{
var discounts = new List();
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);
}
///
/// Gets the product cost (one item)
///
/// Product
/// Shopping cart item attributes in XML
///
/// A task that represents the asynchronous operation
/// The task result contains the product cost (one item)
///
public virtual async Task 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;
}
///
/// Get a price adjustment of a product attribute value
///
/// Product
/// Product attribute value
/// Customer
/// Store
/// Product price (null for using the base product price)
/// Shopping cart item quantity
///
/// A task that represents the asynchronous operation
/// The task result contains the price adjustment
///
public virtual async Task 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;
}
///
/// Round a product or order total for the currency
///
/// Value to round
/// Currency; pass null to use the primary store currency
///
/// A task that represents the asynchronous operation
/// The task result contains the rounded value
///
public virtual async Task 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);
}
///
/// Round
///
/// Value to round
/// The rounding type
/// Rounded value
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
}