1951 lines
88 KiB
C#
1951 lines
88 KiB
C#
using System.Net;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc.Routing;
|
|
using Nop.Core;
|
|
using Nop.Core.Caching;
|
|
using Nop.Core.Domain.Catalog;
|
|
using Nop.Core.Domain.Customers;
|
|
using Nop.Core.Domain.Discounts;
|
|
using Nop.Core.Domain.Orders;
|
|
using Nop.Core.Domain.Stores;
|
|
using Nop.Core.Events;
|
|
using Nop.Data;
|
|
using Nop.Services.Attributes;
|
|
using Nop.Services.Catalog;
|
|
using Nop.Services.Common;
|
|
using Nop.Services.Customers;
|
|
using Nop.Services.Directory;
|
|
using Nop.Services.Helpers;
|
|
using Nop.Services.Localization;
|
|
using Nop.Services.Security;
|
|
using Nop.Services.Seo;
|
|
using Nop.Services.Shipping;
|
|
using Nop.Services.Shipping.Date;
|
|
using Nop.Services.Stores;
|
|
|
|
namespace Nop.Services.Orders;
|
|
|
|
/// <summary>
|
|
/// Shopping cart service
|
|
/// </summary>
|
|
public partial class ShoppingCartService : IShoppingCartService
|
|
{
|
|
#region Fields
|
|
|
|
protected readonly CatalogSettings _catalogSettings;
|
|
protected readonly IAclService _aclService;
|
|
protected readonly IActionContextAccessor _actionContextAccessor;
|
|
protected readonly IAttributeParser<CheckoutAttribute, CheckoutAttributeValue> _checkoutAttributeParser;
|
|
protected readonly IAttributeService<CheckoutAttribute, CheckoutAttributeValue> _checkoutAttributeService;
|
|
protected readonly ICurrencyService _currencyService;
|
|
protected readonly ICustomerService _customerService;
|
|
protected readonly IDateRangeService _dateRangeService;
|
|
protected readonly IDateTimeHelper _dateTimeHelper;
|
|
protected readonly IEventPublisher _eventPublisher;
|
|
protected readonly IGenericAttributeService _genericAttributeService;
|
|
protected readonly IGiftCardService _giftCardService;
|
|
protected readonly ILocalizationService _localizationService;
|
|
protected readonly IPermissionService _permissionService;
|
|
protected readonly IPriceCalculationService _priceCalculationService;
|
|
protected readonly IPriceFormatter _priceFormatter;
|
|
protected readonly IProductAttributeParser _productAttributeParser;
|
|
protected readonly IProductAttributeService _productAttributeService;
|
|
protected readonly IProductService _productService;
|
|
protected readonly IRepository<ShoppingCartItem> _sciRepository;
|
|
protected readonly IShippingService _shippingService;
|
|
protected readonly IShortTermCacheManager _shortTermCacheManager;
|
|
protected readonly IStaticCacheManager _staticCacheManager;
|
|
protected readonly IStoreContext _storeContext;
|
|
protected readonly IStoreService _storeService;
|
|
protected readonly IStoreMappingService _storeMappingService;
|
|
protected readonly IUrlHelperFactory _urlHelperFactory;
|
|
protected readonly IUrlRecordService _urlRecordService;
|
|
protected readonly IWorkContext _workContext;
|
|
protected readonly OrderSettings _orderSettings;
|
|
protected readonly ShoppingCartSettings _shoppingCartSettings;
|
|
|
|
#endregion
|
|
|
|
#region Ctor
|
|
|
|
public ShoppingCartService(CatalogSettings catalogSettings,
|
|
IAclService aclService,
|
|
IActionContextAccessor actionContextAccessor,
|
|
IAttributeParser<CheckoutAttribute, CheckoutAttributeValue> checkoutAttributeParser,
|
|
IAttributeService<CheckoutAttribute, CheckoutAttributeValue> checkoutAttributeService,
|
|
ICurrencyService currencyService,
|
|
ICustomerService customerService,
|
|
IDateRangeService dateRangeService,
|
|
IDateTimeHelper dateTimeHelper,
|
|
IEventPublisher eventPublisher,
|
|
IGenericAttributeService genericAttributeService,
|
|
IGiftCardService giftCardService,
|
|
ILocalizationService localizationService,
|
|
IPermissionService permissionService,
|
|
IPriceCalculationService priceCalculationService,
|
|
IPriceFormatter priceFormatter,
|
|
IProductAttributeParser productAttributeParser,
|
|
IProductAttributeService productAttributeService,
|
|
IProductService productService,
|
|
IRepository<ShoppingCartItem> sciRepository,
|
|
IShippingService shippingService,
|
|
IShortTermCacheManager shortTermCacheManager,
|
|
IStaticCacheManager staticCacheManager,
|
|
IStoreContext storeContext,
|
|
IStoreService storeService,
|
|
IStoreMappingService storeMappingService,
|
|
IUrlHelperFactory urlHelperFactory,
|
|
IUrlRecordService urlRecordService,
|
|
IWorkContext workContext,
|
|
OrderSettings orderSettings,
|
|
ShoppingCartSettings shoppingCartSettings)
|
|
{
|
|
_catalogSettings = catalogSettings;
|
|
_aclService = aclService;
|
|
_actionContextAccessor = actionContextAccessor;
|
|
_checkoutAttributeParser = checkoutAttributeParser;
|
|
_checkoutAttributeService = checkoutAttributeService;
|
|
_currencyService = currencyService;
|
|
_customerService = customerService;
|
|
_dateRangeService = dateRangeService;
|
|
_dateTimeHelper = dateTimeHelper;
|
|
_eventPublisher = eventPublisher;
|
|
_genericAttributeService = genericAttributeService;
|
|
_giftCardService = giftCardService;
|
|
_localizationService = localizationService;
|
|
_permissionService = permissionService;
|
|
_priceCalculationService = priceCalculationService;
|
|
_priceFormatter = priceFormatter;
|
|
_productAttributeParser = productAttributeParser;
|
|
_productAttributeService = productAttributeService;
|
|
_productService = productService;
|
|
_sciRepository = sciRepository;
|
|
_shippingService = shippingService;
|
|
_shortTermCacheManager = shortTermCacheManager;
|
|
_staticCacheManager = staticCacheManager;
|
|
_storeContext = storeContext;
|
|
_storeService = storeService;
|
|
_storeMappingService = storeMappingService;
|
|
_urlHelperFactory = urlHelperFactory;
|
|
_urlRecordService = urlRecordService;
|
|
_workContext = workContext;
|
|
_orderSettings = orderSettings;
|
|
_shoppingCartSettings = shoppingCartSettings;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utilities
|
|
|
|
/// <summary>
|
|
/// Determine if the shopping cart item is the same as the one being compared
|
|
/// </summary>
|
|
/// <param name="shoppingCartItem">Shopping cart item</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <param name="customerEnteredPrice">Price entered by a customer</param>
|
|
/// <param name="rentalStartDate">Rental start date</param>
|
|
/// <param name="rentalEndDate">Rental end date</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the shopping cart item is equal
|
|
/// </returns>
|
|
protected virtual async Task<bool> ShoppingCartItemIsEqualAsync(ShoppingCartItem shoppingCartItem,
|
|
Product product,
|
|
string attributesXml,
|
|
decimal customerEnteredPrice,
|
|
DateTime? rentalStartDate,
|
|
DateTime? rentalEndDate)
|
|
{
|
|
if (shoppingCartItem.ProductId != product.Id)
|
|
return false;
|
|
|
|
//attributes
|
|
var attributesEqual = await _productAttributeParser.AreProductAttributesEqualAsync(shoppingCartItem.AttributesXml, attributesXml, false, false);
|
|
if (!attributesEqual)
|
|
return false;
|
|
|
|
//gift cards
|
|
if (product.IsGiftCard)
|
|
{
|
|
_productAttributeParser.GetGiftCardAttribute(attributesXml, out var giftCardRecipientName1, out var _, out var giftCardSenderName1, out var _, out var _);
|
|
|
|
_productAttributeParser.GetGiftCardAttribute(shoppingCartItem.AttributesXml, out var giftCardRecipientName2, out var _, out var giftCardSenderName2, out var _, out var _);
|
|
|
|
var giftCardsAreEqual = giftCardRecipientName1.Equals(giftCardRecipientName2, StringComparison.InvariantCultureIgnoreCase)
|
|
&& giftCardSenderName1.Equals(giftCardSenderName2, StringComparison.InvariantCultureIgnoreCase);
|
|
if (!giftCardsAreEqual)
|
|
return false;
|
|
}
|
|
|
|
//price is the same (for products which require customers to enter a price)
|
|
if (product.CustomerEntersPrice)
|
|
{
|
|
//we use rounding to eliminate errors associated with storing real numbers in memory when comparing
|
|
var customerEnteredPricesEqual = Math.Round(shoppingCartItem.CustomerEnteredPrice, 2) == Math.Round(customerEnteredPrice, 2);
|
|
if (!customerEnteredPricesEqual)
|
|
return false;
|
|
}
|
|
|
|
if (!product.IsRental)
|
|
return true;
|
|
|
|
//rental products
|
|
var rentalInfoEqual = shoppingCartItem.RentalStartDateUtc == rentalStartDate && shoppingCartItem.RentalEndDateUtc == rentalEndDate;
|
|
|
|
return rentalInfoEqual;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether customer shopping cart is empty
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <returns>Result</returns>
|
|
protected virtual bool IsCustomerShoppingCartEmpty(Customer customer)
|
|
{
|
|
return !_sciRepository.Table.Any(sci => sci.CustomerId == customer.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates required products (products which require some other products to be added to the cart)
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="storeId">Store identifier</param>
|
|
/// <param name="quantity">Quantity</param>
|
|
/// <param name="addRequiredProducts">Whether to add required products</param>
|
|
/// <param name="shoppingCartItemId">Shopping cart identifier; pass 0 if it's a new item</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
protected virtual async Task<IList<string>> GetRequiredProductWarningsAsync(Customer customer, ShoppingCartType shoppingCartType, Product product,
|
|
int storeId, int quantity, bool addRequiredProducts, int shoppingCartItemId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(customer);
|
|
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
//at now we ignore quantities of required products and use 1
|
|
var requiredProductQuantity = 1;
|
|
|
|
//get customer shopping cart
|
|
var cart = await GetShoppingCartAsync(customer, shoppingCartType, storeId);
|
|
|
|
var productsRequiringProduct = await GetProductsRequiringProductAsync(cart, product);
|
|
|
|
//whether other cart items require the passed product
|
|
var passedProductRequiredQuantity = cart.Where(ci => productsRequiringProduct.Any(p => p.Id == ci.ProductId))
|
|
.Sum(item => item.Quantity * requiredProductQuantity);
|
|
|
|
if (passedProductRequiredQuantity > quantity)
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.RequiredProductUpdateWarning"), passedProductRequiredQuantity));
|
|
|
|
//whether the passed product requires other products
|
|
if (!product.RequireOtherProducts)
|
|
return warnings;
|
|
|
|
//get these required products
|
|
var requiredProducts = await _productService.GetProductsByIdsAsync(_productService.ParseRequiredProductIds(product));
|
|
if (!requiredProducts.Any())
|
|
return warnings;
|
|
|
|
var finalRequiredProducts = requiredProducts.GroupBy(p => p.Id)
|
|
.Select(g => new { Product = g.First(), Count = g.Count() });
|
|
|
|
//get warnings
|
|
var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
|
|
var warningLocale = await _localizationService.GetResourceAsync("ShoppingCart.RequiredProductWarning");
|
|
foreach (var requiredProduct in finalRequiredProducts)
|
|
{
|
|
var productsRequiringRequiredProduct = await GetProductsRequiringProductAsync(cart, requiredProduct.Product);
|
|
|
|
//get the required quantity of the required product
|
|
var requiredProductRequiredQuantity = quantity * requiredProductQuantity +
|
|
cart.Where(ci => productsRequiringRequiredProduct.Any(p => p.Id == ci.ProductId))
|
|
.Where(item => item.Id != shoppingCartItemId)
|
|
.Sum(item => item.Quantity * requiredProductQuantity);
|
|
|
|
//whether required product is already in the cart in the required quantity
|
|
var quantityToAdd = requiredProductRequiredQuantity * requiredProduct.Count - (cart.FirstOrDefault(item => item.ProductId == requiredProduct.Product.Id)?.Quantity ?? 0);
|
|
if (quantityToAdd <= 0)
|
|
continue;
|
|
|
|
//prepare warning message
|
|
var url = urlHelper.RouteUrl(nameof(Product), new { SeName = await _urlRecordService.GetSeNameAsync(requiredProduct.Product) });
|
|
var requiredProductName = WebUtility.HtmlEncode(await _localizationService.GetLocalizedAsync(requiredProduct.Product, x => x.Name));
|
|
var requiredProductWarning = _catalogSettings.UseLinksInRequiredProductWarnings
|
|
? string.Format(warningLocale, $"<a href=\"{url}\">{requiredProductName}</a>", requiredProductRequiredQuantity * requiredProduct.Count)
|
|
: string.Format(warningLocale, requiredProductName, requiredProductRequiredQuantity);
|
|
|
|
//add to cart (if possible)
|
|
if (addRequiredProducts && product.AutomaticallyAddRequiredProducts)
|
|
{
|
|
//do not add required products to prevent circular references
|
|
var addToCartWarnings = await GetShoppingCartItemWarningsAsync(
|
|
customer: customer,
|
|
product: requiredProduct.Product,
|
|
attributesXml: null,
|
|
customerEnteredPrice: decimal.Zero,
|
|
shoppingCartType: shoppingCartType,
|
|
storeId: storeId,
|
|
quantity: quantityToAdd,
|
|
addRequiredProducts: true);
|
|
|
|
//don't display all specific errors only the generic one
|
|
if (addToCartWarnings.Any())
|
|
warnings.Add(requiredProductWarning);
|
|
}
|
|
else
|
|
{
|
|
warnings.Add(requiredProductWarning);
|
|
}
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a product for standard properties
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <param name="customerEnteredPrice">Customer entered price</param>
|
|
/// <param name="quantity">Quantity</param>
|
|
/// <param name="shoppingCartItemId">Shopping cart identifier; pass 0 if it's a new item</param>
|
|
/// <param name="storeId">Store identifier</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
protected virtual async Task<IList<string>> GetStandardWarningsAsync(Customer customer, ShoppingCartType shoppingCartType, Product product,
|
|
string attributesXml, decimal customerEnteredPrice, int quantity, int shoppingCartItemId, int storeId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(customer);
|
|
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
//deleted
|
|
if (product.Deleted)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.ProductDeleted"));
|
|
return warnings;
|
|
}
|
|
|
|
//published
|
|
if (!product.Published)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.ProductUnpublished"));
|
|
}
|
|
|
|
//we can add only simple products
|
|
if (product.ProductType != ProductType.SimpleProduct)
|
|
{
|
|
warnings.Add("This is not simple product");
|
|
}
|
|
|
|
//ACL
|
|
if (!await _aclService.AuthorizeAsync(product, customer))
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.ProductUnpublished"));
|
|
}
|
|
|
|
//Store mapping
|
|
if (!await _storeMappingService.AuthorizeAsync(product, storeId))
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.ProductUnpublished"));
|
|
}
|
|
|
|
//disabled "add to cart" button
|
|
if (shoppingCartType == ShoppingCartType.ShoppingCart && product.DisableBuyButton)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.BuyingDisabled"));
|
|
}
|
|
|
|
//disabled "add to wishlist" button
|
|
if (shoppingCartType == ShoppingCartType.Wishlist && product.DisableWishlistButton)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.WishlistDisabled"));
|
|
}
|
|
|
|
//call for price
|
|
if (shoppingCartType == ShoppingCartType.ShoppingCart && product.CallForPrice &&
|
|
//also check whether the current user is impersonated
|
|
(!_orderSettings.AllowAdminsToBuyCallForPriceProducts || _workContext.OriginalCustomerIfImpersonated == null))
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("Products.CallForPrice"));
|
|
}
|
|
|
|
//customer entered price
|
|
if (product.CustomerEntersPrice)
|
|
{
|
|
if (customerEnteredPrice < product.MinimumCustomerEnteredPrice ||
|
|
customerEnteredPrice > product.MaximumCustomerEnteredPrice)
|
|
{
|
|
var currentCurrency = await _workContext.GetWorkingCurrencyAsync();
|
|
var minimumCustomerEnteredPrice = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(product.MinimumCustomerEnteredPrice, currentCurrency);
|
|
var maximumCustomerEnteredPrice = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(product.MaximumCustomerEnteredPrice, currentCurrency);
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.CustomerEnteredPrice.RangeError"),
|
|
await _priceFormatter.FormatPriceAsync(minimumCustomerEnteredPrice, false, false),
|
|
await _priceFormatter.FormatPriceAsync(maximumCustomerEnteredPrice, false, false)));
|
|
}
|
|
}
|
|
|
|
//quantity validation
|
|
var hasQtyWarnings = false;
|
|
if (quantity < product.OrderMinimumQuantity)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.MinimumQuantity"), product.OrderMinimumQuantity));
|
|
hasQtyWarnings = true;
|
|
}
|
|
|
|
if (quantity > product.OrderMaximumQuantity)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.MaximumQuantity"), product.OrderMaximumQuantity));
|
|
hasQtyWarnings = true;
|
|
}
|
|
|
|
var allowedQuantities = _productService.ParseAllowedQuantities(product);
|
|
if (allowedQuantities.Length > 0 && !allowedQuantities.Contains(quantity))
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.AllowedQuantities"), string.Join(", ", allowedQuantities)));
|
|
}
|
|
|
|
var validateOutOfStock = shoppingCartType == ShoppingCartType.ShoppingCart || !_shoppingCartSettings.AllowOutOfStockItemsToBeAddedToWishlist;
|
|
if (validateOutOfStock && !hasQtyWarnings)
|
|
{
|
|
switch (product.ManageInventoryMethod)
|
|
{
|
|
case ManageInventoryMethod.DontManageStock:
|
|
//do nothing
|
|
break;
|
|
case ManageInventoryMethod.ManageStock:
|
|
if (product.BackorderMode == BackorderMode.NoBackorders)
|
|
{
|
|
var maximumQuantityCanBeAdded = await _productService.GetTotalStockQuantityAsync(product);
|
|
|
|
warnings.AddRange(await GetQuantityProductWarningsAsync(product, quantity, maximumQuantityCanBeAdded));
|
|
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
//validate product quantity with non combinable product attributes
|
|
var productAttributeMappings = await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id);
|
|
if (productAttributeMappings?.Any() == true)
|
|
{
|
|
var onlyCombinableAttributes = productAttributeMappings.All(mapping => !mapping.IsNonCombinable());
|
|
if (!onlyCombinableAttributes)
|
|
{
|
|
var cart = await GetShoppingCartAsync(customer, shoppingCartType, storeId);
|
|
var totalAddedQuantity = cart
|
|
.Where(item => item.ProductId == product.Id && item.Id != shoppingCartItemId)
|
|
.Sum(product => product.Quantity);
|
|
|
|
totalAddedQuantity += quantity;
|
|
|
|
//counting a product into bundles
|
|
foreach (var bundle in cart.Where(x => x.Id != shoppingCartItemId && !string.IsNullOrEmpty(x.AttributesXml)))
|
|
{
|
|
var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(bundle.AttributesXml);
|
|
foreach (var attributeValue in attributeValues)
|
|
{
|
|
if (attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct && attributeValue.AssociatedProductId == product.Id)
|
|
totalAddedQuantity += bundle.Quantity * attributeValue.Quantity;
|
|
}
|
|
}
|
|
|
|
warnings.AddRange(await GetQuantityProductWarningsAsync(product, totalAddedQuantity, maximumQuantityCanBeAdded));
|
|
}
|
|
}
|
|
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
//validate product quantity and product quantity into bundles
|
|
if (string.IsNullOrEmpty(attributesXml))
|
|
{
|
|
var cart = await GetShoppingCartAsync(customer, shoppingCartType, storeId);
|
|
var totalQuantityInCart = cart.Where(item => item.ProductId == product.Id && item.Id != shoppingCartItemId && string.IsNullOrEmpty(item.AttributesXml))
|
|
.Sum(product => product.Quantity);
|
|
|
|
totalQuantityInCart += quantity;
|
|
|
|
foreach (var bundle in cart.Where(x => x.Id != shoppingCartItemId && !string.IsNullOrEmpty(x.AttributesXml)))
|
|
{
|
|
var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(bundle.AttributesXml);
|
|
foreach (var attributeValue in attributeValues)
|
|
{
|
|
if (attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct && attributeValue.AssociatedProductId == product.Id)
|
|
totalQuantityInCart += bundle.Quantity * attributeValue.Quantity;
|
|
}
|
|
}
|
|
|
|
warnings.AddRange(await GetQuantityProductWarningsAsync(product, totalQuantityInCart, maximumQuantityCanBeAdded));
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ManageInventoryMethod.ManageStockByAttributes:
|
|
var combination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml);
|
|
if (combination != null)
|
|
{
|
|
//combination exists
|
|
//let's check stock level
|
|
if (!combination.AllowOutOfStockOrders)
|
|
warnings.AddRange(await GetQuantityProductWarningsAsync(product, quantity, combination.StockQuantity));
|
|
}
|
|
else
|
|
{
|
|
//combination doesn't exist
|
|
if (product.AllowAddingOnlyExistingAttributeCombinations)
|
|
{
|
|
//maybe, is it better to display something like "No such product/combination" message?
|
|
var productAvailabilityRange = await _dateRangeService.GetProductAvailabilityRangeByIdAsync(product.ProductAvailabilityRangeId);
|
|
var warning = productAvailabilityRange == null ? await _localizationService.GetResourceAsync("ShoppingCart.OutOfStock")
|
|
: string.Format(await _localizationService.GetResourceAsync("ShoppingCart.AvailabilityRange"),
|
|
await _localizationService.GetLocalizedAsync(productAvailabilityRange, range => range.Name));
|
|
warnings.Add(warning);
|
|
}
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
//availability dates
|
|
var availableStartDateError = false;
|
|
if (product.AvailableStartDateTimeUtc.HasValue)
|
|
{
|
|
var availableStartDateTime = DateTime.SpecifyKind(product.AvailableStartDateTimeUtc.Value, DateTimeKind.Utc);
|
|
if (availableStartDateTime.CompareTo(DateTime.UtcNow) > 0)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.NotAvailable"));
|
|
availableStartDateError = true;
|
|
}
|
|
}
|
|
|
|
if (!product.AvailableEndDateTimeUtc.HasValue || availableStartDateError)
|
|
return warnings;
|
|
|
|
var availableEndDateTime = DateTime.SpecifyKind(product.AvailableEndDateTimeUtc.Value, DateTimeKind.Utc);
|
|
if (availableEndDateTime.CompareTo(DateTime.UtcNow) < 0)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.NotAvailable"));
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the maximum quantity a product can be added
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="quantity">Quantity</param>
|
|
/// <param name="maximumQuantityCanBeAdded">The maximum quantity a product can be added</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
protected virtual async Task<IList<string>> GetQuantityProductWarningsAsync(Product product, int quantity, int maximumQuantityCanBeAdded)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
if (maximumQuantityCanBeAdded < quantity)
|
|
{
|
|
if (maximumQuantityCanBeAdded <= 0)
|
|
{
|
|
var productAvailabilityRange = await _dateRangeService.GetProductAvailabilityRangeByIdAsync(product.ProductAvailabilityRangeId);
|
|
var warning = productAvailabilityRange == null ? await _localizationService.GetResourceAsync("ShoppingCart.OutOfStock")
|
|
: string.Format(await _localizationService.GetResourceAsync("ShoppingCart.AvailabilityRange"),
|
|
await _localizationService.GetLocalizedAsync(productAvailabilityRange, range => range.Name));
|
|
warnings.Add(warning);
|
|
}
|
|
else
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.QuantityExceedsStock"), maximumQuantityCanBeAdded));
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Delete shopping cart item
|
|
/// </summary>
|
|
/// <param name="shoppingCartItem">Shopping cart item</param>
|
|
/// <param name="resetCheckoutData">A value indicating whether to reset checkout data</param>
|
|
/// <param name="ensureOnlyActiveCheckoutAttributes">A value indicating whether to ensure that only active checkout attributes are attached to the current customer</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public virtual async Task DeleteShoppingCartItemAsync(ShoppingCartItem shoppingCartItem, bool resetCheckoutData = true,
|
|
bool ensureOnlyActiveCheckoutAttributes = false)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(shoppingCartItem);
|
|
|
|
var customer = await _customerService.GetCustomerByIdAsync(shoppingCartItem.CustomerId);
|
|
var storeId = shoppingCartItem.StoreId;
|
|
|
|
//reset checkout data
|
|
if (resetCheckoutData)
|
|
await _customerService.ResetCheckoutDataAsync(customer, shoppingCartItem.StoreId);
|
|
|
|
//delete item
|
|
await _sciRepository.DeleteAsync(shoppingCartItem);
|
|
|
|
//reset "HasShoppingCartItems" property used for performance optimization
|
|
var hasShoppingCartItems = !IsCustomerShoppingCartEmpty(customer);
|
|
if (hasShoppingCartItems != customer.HasShoppingCartItems)
|
|
{
|
|
customer.HasShoppingCartItems = hasShoppingCartItems;
|
|
await _customerService.UpdateCustomerAsync(customer);
|
|
}
|
|
|
|
//validate checkout attributes
|
|
if (ensureOnlyActiveCheckoutAttributes &&
|
|
//only for shopping cart items (ignore wishlist)
|
|
shoppingCartItem.ShoppingCartType == ShoppingCartType.ShoppingCart)
|
|
{
|
|
var cart = await GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, storeId);
|
|
|
|
var checkoutAttributesXml =
|
|
await _genericAttributeService.GetAttributeAsync<string>(customer, NopCustomerDefaults.CheckoutAttributes,
|
|
storeId);
|
|
checkoutAttributesXml =
|
|
await _checkoutAttributeParser.EnsureOnlyActiveAttributesAsync(checkoutAttributesXml, cart);
|
|
await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.CheckoutAttributes,
|
|
checkoutAttributesXml, storeId);
|
|
}
|
|
|
|
if (!_catalogSettings.RemoveRequiredProducts)
|
|
return;
|
|
|
|
var product = await _productService.GetProductByIdAsync(shoppingCartItem.ProductId);
|
|
if (!product?.RequireOtherProducts ?? true)
|
|
return;
|
|
|
|
var requiredProductIds = _productService.ParseRequiredProductIds(product);
|
|
var requiredShoppingCartItems =
|
|
(await GetShoppingCartAsync(customer, shoppingCartType: shoppingCartItem.ShoppingCartType))
|
|
.Where(item => requiredProductIds.Any(id => id == item.ProductId))
|
|
.ToList();
|
|
|
|
//update quantity of required products in the cart if the main one is removed
|
|
foreach (var cartItem in requiredShoppingCartItems)
|
|
{
|
|
//at now we ignore quantities of required products and use 1
|
|
var requiredProductQuantity = 1;
|
|
|
|
await UpdateShoppingCartItemAsync(customer, cartItem.Id, cartItem.AttributesXml, cartItem.CustomerEnteredPrice,
|
|
quantity: cartItem.Quantity - shoppingCartItem.Quantity * requiredProductQuantity,
|
|
resetCheckoutData: false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear shopping cart
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="storeId">Store ID</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public virtual async Task ClearShoppingCartAsync(Customer customer, int storeId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(customer);
|
|
|
|
var cart = await GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, storeId);
|
|
|
|
//delete items
|
|
await _sciRepository.DeleteAsync(cart, publishEvent: false);
|
|
await _eventPublisher.PublishAsync(new ClearShoppingCartEvent(cart));
|
|
|
|
//reset "HasShoppingCartItems" property used for performance optimization
|
|
var hasShoppingCartItems = !IsCustomerShoppingCartEmpty(customer);
|
|
if (hasShoppingCartItems != customer.HasShoppingCartItems)
|
|
{
|
|
customer.HasShoppingCartItems = hasShoppingCartItems;
|
|
await _customerService.UpdateCustomerAsync(customer);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete shopping cart item
|
|
/// </summary>
|
|
/// <param name="shoppingCartItemId">Shopping cart item ID</param>
|
|
/// <param name="resetCheckoutData">A value indicating whether to reset checkout data</param>
|
|
/// <param name="ensureOnlyActiveCheckoutAttributes">A value indicating whether to ensure that only active checkout attributes are attached to the current customer</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public virtual async Task DeleteShoppingCartItemAsync(int shoppingCartItemId, bool resetCheckoutData = true,
|
|
bool ensureOnlyActiveCheckoutAttributes = false)
|
|
{
|
|
var shoppingCartItem = await _sciRepository.Table.FirstOrDefaultAsync(sci => sci.Id == shoppingCartItemId);
|
|
if (shoppingCartItem != null)
|
|
await DeleteShoppingCartItemAsync(shoppingCartItem, resetCheckoutData, ensureOnlyActiveCheckoutAttributes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes expired shopping cart items
|
|
/// </summary>
|
|
/// <param name="olderThanUtc">Older than date and time</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the number of deleted items
|
|
/// </returns>
|
|
public virtual async Task<int> DeleteExpiredShoppingCartItemsAsync(DateTime olderThanUtc)
|
|
{
|
|
var query = from sci in _sciRepository.Table
|
|
where sci.UpdatedOnUtc < olderThanUtc
|
|
select sci;
|
|
|
|
var cartItems = await query.ToListAsync();
|
|
|
|
foreach (var cartItem in cartItems)
|
|
await DeleteShoppingCartItemAsync(cartItem);
|
|
|
|
return cartItems.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get products from shopping cart whether requiring specific product
|
|
/// </summary>
|
|
/// <param name="cart">Shopping cart </param>
|
|
/// <param name="product">Product</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the result
|
|
/// </returns>
|
|
public virtual async Task<IList<Product>> GetProductsRequiringProductAsync(IList<ShoppingCartItem> cart, Product product)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(cart);
|
|
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
if (!cart.Any())
|
|
return new List<Product>();
|
|
|
|
var productIds = cart.Select(ci => ci.ProductId).ToArray();
|
|
|
|
var cartProducts = await _productService.GetProductsByIdsAsync(productIds);
|
|
|
|
return cartProducts.Where(cartProduct =>
|
|
cartProduct.RequireOtherProducts &&
|
|
_productService.ParseRequiredProductIds(cartProduct).Contains(product.Id)).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets shopping cart
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="shoppingCartType">Shopping cart type; pass null to load all records</param>
|
|
/// <param name="storeId">Store identifier; pass 0 to load all records</param>
|
|
/// <param name="productId">Product identifier; pass null to load all records</param>
|
|
/// <param name="createdFromUtc">Created date from (UTC); pass null to load all records</param>
|
|
/// <param name="createdToUtc">Created date to (UTC); pass null to load all records</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the shopping Cart
|
|
/// </returns>
|
|
public virtual async Task<IList<ShoppingCartItem>> GetShoppingCartAsync(Customer customer, ShoppingCartType? shoppingCartType = null,
|
|
int storeId = 0, int? productId = null, DateTime? createdFromUtc = null, DateTime? createdToUtc = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(customer);
|
|
|
|
var items = _sciRepository.Table.Where(sci => sci.CustomerId == customer.Id);
|
|
|
|
//filter by type
|
|
if (shoppingCartType.HasValue)
|
|
items = items.Where(item => item.ShoppingCartTypeId == (int)shoppingCartType.Value);
|
|
|
|
//filter shopping cart items by store
|
|
if (storeId > 0 && !_shoppingCartSettings.CartsSharedBetweenStores)
|
|
items = items.Where(item => item.StoreId == storeId);
|
|
|
|
//filter shopping cart items by product
|
|
if (productId > 0)
|
|
items = items.Where(item => item.ProductId == productId);
|
|
|
|
//filter shopping cart items by date
|
|
if (createdFromUtc.HasValue)
|
|
items = items.Where(item => createdFromUtc.Value <= item.CreatedOnUtc);
|
|
if (createdToUtc.HasValue)
|
|
items = items.Where(item => createdToUtc.Value >= item.CreatedOnUtc);
|
|
|
|
return await _shortTermCacheManager.GetAsync(async () => await items.ToListAsync(), NopOrderDefaults.ShoppingCartItemsAllCacheKey, customer, shoppingCartType, storeId, productId, createdFromUtc, createdToUtc);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates shopping cart item attributes
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="quantity">Quantity</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <param name="ignoreNonCombinableAttributes">A value indicating whether we should ignore non-combinable attributes</param>
|
|
/// <param name="ignoreConditionMet">A value indicating whether we should ignore filtering by "is condition met" property</param>
|
|
/// <param name="ignoreBundledProducts">A value indicating whether we should ignore bundled (associated) products</param>
|
|
/// <param name="shoppingCartItemId">Shopping cart identifier; pass 0 if it's a new item</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
public virtual async Task<IList<string>> GetShoppingCartItemAttributeWarningsAsync(Customer customer,
|
|
ShoppingCartType shoppingCartType,
|
|
Product product,
|
|
int quantity = 1,
|
|
string attributesXml = "",
|
|
bool ignoreNonCombinableAttributes = false,
|
|
bool ignoreConditionMet = false,
|
|
bool ignoreBundledProducts = false,
|
|
int shoppingCartItemId = 0)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
//ensure it's our attributes
|
|
var attributes1 = await _productAttributeParser.ParseProductAttributeMappingsAsync(attributesXml);
|
|
if (ignoreNonCombinableAttributes)
|
|
{
|
|
attributes1 = attributes1.Where(x => !x.IsNonCombinable()).ToList();
|
|
}
|
|
|
|
foreach (var attribute in attributes1)
|
|
{
|
|
if (attribute.ProductId == 0)
|
|
{
|
|
warnings.Add("Attribute error");
|
|
return warnings;
|
|
}
|
|
|
|
if (attribute.ProductId != product.Id)
|
|
{
|
|
warnings.Add("Attribute error");
|
|
}
|
|
}
|
|
|
|
//validate required product attributes (whether they're chosen/selected/entered)
|
|
var attributes2 = await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id);
|
|
if (ignoreNonCombinableAttributes)
|
|
{
|
|
attributes2 = attributes2.Where(x => !x.IsNonCombinable()).ToList();
|
|
}
|
|
|
|
//validate conditional attributes only (if specified)
|
|
if (!ignoreConditionMet)
|
|
{
|
|
attributes2 = await attributes2.WhereAwait(async x =>
|
|
{
|
|
var conditionMet = await _productAttributeParser.IsConditionMetAsync(x, attributesXml);
|
|
return !conditionMet.HasValue || conditionMet.Value;
|
|
}).ToListAsync();
|
|
}
|
|
|
|
foreach (var a2 in attributes2)
|
|
{
|
|
var productAttributeValues = await _productAttributeService.GetProductAttributeValuesAsync(a2.Id);
|
|
|
|
if (a2.IsRequired)
|
|
{
|
|
var found = false;
|
|
//selected product attributes
|
|
foreach (var a1 in attributes1)
|
|
{
|
|
if (a1.Id != a2.Id)
|
|
continue;
|
|
|
|
var attributeValuesStr = _productAttributeParser.ParseValues(attributesXml, a1.Id);
|
|
|
|
if (a2.ShouldHaveValues() && productAttributeValues.Any() && !productAttributeValues.Any(x => attributeValuesStr.Contains(x.Id.ToString())))
|
|
break;
|
|
|
|
foreach (var str1 in attributeValuesStr)
|
|
{
|
|
if (string.IsNullOrEmpty(str1.Trim()))
|
|
continue;
|
|
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//if not found
|
|
if (!found)
|
|
{
|
|
var productAttribute = await _productAttributeService.GetProductAttributeByIdAsync(a2.ProductAttributeId);
|
|
|
|
var textPrompt = await _localizationService.GetLocalizedAsync(a2, x => x.TextPrompt);
|
|
var notFoundWarning = !string.IsNullOrEmpty(textPrompt) ?
|
|
textPrompt :
|
|
string.Format(await _localizationService.GetResourceAsync("ShoppingCart.SelectAttribute"), await _localizationService.GetLocalizedAsync(productAttribute, a => a.Name));
|
|
|
|
warnings.Add(notFoundWarning);
|
|
}
|
|
}
|
|
|
|
if (a2.AttributeControlType != AttributeControlType.ReadonlyCheckboxes)
|
|
continue;
|
|
|
|
//customers cannot edit read-only attributes
|
|
var allowedReadOnlyValueIds = productAttributeValues
|
|
.Where(x => x.IsPreSelected)
|
|
.Select(x => x.Id)
|
|
.ToArray();
|
|
|
|
var selectedReadOnlyValueIds = (await _productAttributeParser.ParseProductAttributeValuesAsync(attributesXml))
|
|
.Where(x => x.ProductAttributeMappingId == a2.Id)
|
|
.Select(x => x.Id)
|
|
.ToArray();
|
|
|
|
if (!CommonHelper.ArraysEqual(allowedReadOnlyValueIds, selectedReadOnlyValueIds))
|
|
{
|
|
warnings.Add("You cannot change read-only values");
|
|
}
|
|
}
|
|
|
|
//validation rules
|
|
foreach (var pam in attributes2)
|
|
{
|
|
if (!pam.ValidationRulesAllowed())
|
|
continue;
|
|
|
|
string enteredText;
|
|
int enteredTextLength;
|
|
|
|
var productAttribute = await _productAttributeService.GetProductAttributeByIdAsync(pam.ProductAttributeId);
|
|
|
|
//minimum length
|
|
if (pam.ValidationMinLength.HasValue)
|
|
{
|
|
if (pam.AttributeControlType == AttributeControlType.TextBox ||
|
|
pam.AttributeControlType == AttributeControlType.MultilineTextbox)
|
|
{
|
|
enteredText = _productAttributeParser.ParseValues(attributesXml, pam.Id).FirstOrDefault();
|
|
enteredTextLength = string.IsNullOrEmpty(enteredText) ? 0 : enteredText.Length;
|
|
|
|
if (pam.ValidationMinLength.Value > enteredTextLength)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.TextboxMinimumLength"), await _localizationService.GetLocalizedAsync(productAttribute, a => a.Name), pam.ValidationMinLength.Value));
|
|
}
|
|
}
|
|
}
|
|
|
|
//maximum length
|
|
if (!pam.ValidationMaxLength.HasValue)
|
|
continue;
|
|
|
|
if (pam.AttributeControlType != AttributeControlType.TextBox && pam.AttributeControlType != AttributeControlType.MultilineTextbox)
|
|
continue;
|
|
|
|
enteredText = _productAttributeParser.ParseValues(attributesXml, pam.Id).FirstOrDefault();
|
|
enteredTextLength = string.IsNullOrEmpty(enteredText) ? 0 : enteredText.Length;
|
|
|
|
if (pam.ValidationMaxLength.Value < enteredTextLength)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.TextboxMaximumLength"), await _localizationService.GetLocalizedAsync(productAttribute, a => a.Name), pam.ValidationMaxLength.Value));
|
|
}
|
|
}
|
|
|
|
if (warnings.Any() || ignoreBundledProducts)
|
|
return warnings;
|
|
|
|
//validate bundled products
|
|
var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(attributesXml);
|
|
foreach (var attributeValue in attributeValues)
|
|
{
|
|
if (attributeValue.AttributeValueType != AttributeValueType.AssociatedToProduct)
|
|
continue;
|
|
|
|
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(attributeValue.ProductAttributeMappingId);
|
|
|
|
if (productAttributeMapping == null)
|
|
continue;
|
|
|
|
if (ignoreNonCombinableAttributes && productAttributeMapping.IsNonCombinable())
|
|
continue;
|
|
|
|
//associated product (bundle)
|
|
var associatedProduct = await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId);
|
|
|
|
if (associatedProduct != null)
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var totalQty = quantity * attributeValue.Quantity;
|
|
var associatedProductWarnings = await GetShoppingCartItemWarningsAsync(customer,
|
|
shoppingCartType, associatedProduct, store.Id,
|
|
string.Empty, decimal.Zero, null, null, totalQty, false, shoppingCartItemId);
|
|
|
|
var productAttribute = await _productAttributeService.GetProductAttributeByIdAsync(productAttributeMapping.ProductAttributeId);
|
|
|
|
foreach (var associatedProductWarning in associatedProductWarnings)
|
|
{
|
|
var attributeName = await _localizationService.GetLocalizedAsync(productAttribute, a => a.Name);
|
|
var attributeValueName = await _localizationService.GetLocalizedAsync(attributeValue, a => a.Name);
|
|
warnings.Add(string.Format(
|
|
await _localizationService.GetResourceAsync("ShoppingCart.AssociatedAttributeWarning"),
|
|
attributeName, attributeValueName, associatedProductWarning));
|
|
}
|
|
}
|
|
else
|
|
warnings.Add($"Associated product cannot be loaded - {attributeValue.AssociatedProductId}");
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates shopping cart item (gift card)
|
|
/// </summary>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
public virtual async Task<IList<string>> GetShoppingCartItemGiftCardWarningsAsync(ShoppingCartType shoppingCartType,
|
|
Product product, string attributesXml)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
//gift cards
|
|
if (!product.IsGiftCard)
|
|
return warnings;
|
|
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
var giftCards = await _giftCardService.GetActiveGiftCardsAppliedByCustomerAsync(customer);
|
|
if (giftCards.Any())
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.GiftCardCouponCode.DontWorkWithGiftCards"));
|
|
|
|
_productAttributeParser.GetGiftCardAttribute(attributesXml, out var giftCardRecipientName, out var giftCardRecipientEmail, out var giftCardSenderName, out var giftCardSenderEmail, out var _);
|
|
|
|
if (string.IsNullOrEmpty(giftCardRecipientName))
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.RecipientNameError"));
|
|
|
|
if (product.GiftCardType == GiftCardType.Virtual)
|
|
{
|
|
//validate for virtual gift cards only
|
|
if (string.IsNullOrEmpty(giftCardRecipientEmail) || !CommonHelper.IsValidEmail(giftCardRecipientEmail))
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.RecipientEmailError"));
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(giftCardSenderName))
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.SenderNameError"));
|
|
|
|
if (product.GiftCardType != GiftCardType.Virtual)
|
|
return warnings;
|
|
|
|
//validate for virtual gift cards only
|
|
if (string.IsNullOrEmpty(giftCardSenderEmail) || !CommonHelper.IsValidEmail(giftCardSenderEmail))
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.SenderEmailError"));
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates shopping cart item for rental products
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="rentalStartDate">Rental start date</param>
|
|
/// <param name="rentalEndDate">Rental end date</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
public virtual async Task<IList<string>> GetRentalProductWarningsAsync(Product product,
|
|
DateTime? rentalStartDate = null, DateTime? rentalEndDate = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
if (!product.IsRental)
|
|
return warnings;
|
|
|
|
if (!rentalStartDate.HasValue)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.Rental.EnterStartDate"));
|
|
return warnings;
|
|
}
|
|
|
|
if (!rentalEndDate.HasValue)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.Rental.EnterEndDate"));
|
|
return warnings;
|
|
}
|
|
|
|
if (rentalStartDate.Value.CompareTo(rentalEndDate.Value) > 0)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.Rental.StartDateLessEndDate"));
|
|
return warnings;
|
|
}
|
|
|
|
//allowed start date should be the future date
|
|
//we should compare rental start date with a store local time
|
|
//but we what if a store works in distinct timezones? how we should handle it? skip it for now
|
|
//we also ignore hours (anyway not supported yet)
|
|
//today
|
|
var nowDtInStoreTimeZone = _dateTimeHelper.ConvertToUserTime(DateTime.Now, TimeZoneInfo.Local, _dateTimeHelper.DefaultStoreTimeZone);
|
|
var todayDt = new DateTime(nowDtInStoreTimeZone.Year, nowDtInStoreTimeZone.Month, nowDtInStoreTimeZone.Day);
|
|
var todayDtUtc = _dateTimeHelper.ConvertToUtcTime(todayDt, _dateTimeHelper.DefaultStoreTimeZone);
|
|
//dates are entered in store timezone (e.g. like in hotels)
|
|
var startDateUtc = _dateTimeHelper.ConvertToUtcTime(rentalStartDate.Value, _dateTimeHelper.DefaultStoreTimeZone);
|
|
//but we what if dates should be entered in a customer timezone?
|
|
//DateTime startDateUtc = _dateTimeHelper.ConvertToUtcTime(rentalStartDate.Value, _dateTimeHelper.CurrentTimeZone);
|
|
if (todayDtUtc.CompareTo(startDateUtc) <= 0)
|
|
return warnings;
|
|
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.Rental.StartDateShouldBeFuture"));
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates shopping cart item
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="storeId">Store identifier</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <param name="customerEnteredPrice">Customer entered price</param>
|
|
/// <param name="rentalStartDate">Rental start date</param>
|
|
/// <param name="rentalEndDate">Rental end date</param>
|
|
/// <param name="quantity">Quantity</param>
|
|
/// <param name="addRequiredProducts">Whether to add required products</param>
|
|
/// <param name="shoppingCartItemId">Shopping cart identifier; pass 0 if it's a new item</param>
|
|
/// <param name="getStandardWarnings">A value indicating whether we should validate a product for standard properties</param>
|
|
/// <param name="getAttributesWarnings">A value indicating whether we should validate product attributes</param>
|
|
/// <param name="getGiftCardWarnings">A value indicating whether we should validate gift card properties</param>
|
|
/// <param name="getRequiredProductWarnings">A value indicating whether we should validate required products (products which require other products to be added to the cart)</param>
|
|
/// <param name="getRentalWarnings">A value indicating whether we should validate rental properties</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
public virtual async Task<IList<string>> GetShoppingCartItemWarningsAsync(Customer customer, ShoppingCartType shoppingCartType,
|
|
Product product, int storeId,
|
|
string attributesXml, decimal customerEnteredPrice,
|
|
DateTime? rentalStartDate = null, DateTime? rentalEndDate = null,
|
|
int quantity = 1, bool addRequiredProducts = true, int shoppingCartItemId = 0,
|
|
bool getStandardWarnings = true, bool getAttributesWarnings = true,
|
|
bool getGiftCardWarnings = true, bool getRequiredProductWarnings = true,
|
|
bool getRentalWarnings = true)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
//standard properties
|
|
if (getStandardWarnings)
|
|
warnings.AddRange(await GetStandardWarningsAsync(customer, shoppingCartType, product, attributesXml, customerEnteredPrice, quantity, shoppingCartItemId, storeId));
|
|
|
|
//selected attributes
|
|
if (getAttributesWarnings)
|
|
warnings.AddRange(await GetShoppingCartItemAttributeWarningsAsync(customer, shoppingCartType, product, quantity, attributesXml, false, false, false, shoppingCartItemId));
|
|
|
|
//gift cards
|
|
if (getGiftCardWarnings)
|
|
warnings.AddRange(await GetShoppingCartItemGiftCardWarningsAsync(shoppingCartType, product, attributesXml));
|
|
|
|
//required products
|
|
if (getRequiredProductWarnings)
|
|
warnings.AddRange(await GetRequiredProductWarningsAsync(customer, shoppingCartType, product, storeId, quantity, addRequiredProducts, shoppingCartItemId));
|
|
|
|
//rental products
|
|
if (getRentalWarnings)
|
|
warnings.AddRange(await GetRentalProductWarningsAsync(product, rentalStartDate, rentalEndDate));
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates whether this shopping cart is valid
|
|
/// </summary>
|
|
/// <param name="shoppingCart">Shopping cart</param>
|
|
/// <param name="checkoutAttributesXml">Checkout attributes in XML format</param>
|
|
/// <param name="validateCheckoutAttributes">A value indicating whether to validate checkout attributes</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
public virtual async Task<IList<string>> GetShoppingCartWarningsAsync(IList<ShoppingCartItem> shoppingCart,
|
|
string checkoutAttributesXml, bool validateCheckoutAttributes)
|
|
{
|
|
var warnings = new List<string>();
|
|
|
|
if (shoppingCart.Count > _shoppingCartSettings.MaximumShoppingCartItems)
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.MaximumShoppingCartItems"), _shoppingCartSettings.MaximumShoppingCartItems));
|
|
|
|
var hasStandardProducts = false;
|
|
var hasRecurringProducts = false;
|
|
|
|
foreach (var sci in shoppingCart)
|
|
{
|
|
var product = await _productService.GetProductByIdAsync(sci.ProductId);
|
|
if (product == null)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.CannotLoadProduct"), sci.ProductId));
|
|
return warnings;
|
|
}
|
|
|
|
if (product.IsRecurring)
|
|
hasRecurringProducts = true;
|
|
else
|
|
hasStandardProducts = true;
|
|
}
|
|
|
|
//don't mix standard and recurring products
|
|
if (hasStandardProducts && hasRecurringProducts)
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.CannotMixStandardAndAutoshipProducts"));
|
|
|
|
//recurring cart validation
|
|
if (hasRecurringProducts)
|
|
{
|
|
var cyclesError = (await GetRecurringCycleInfoAsync(shoppingCart)).error;
|
|
if (!string.IsNullOrEmpty(cyclesError))
|
|
{
|
|
warnings.Add(cyclesError);
|
|
return warnings;
|
|
}
|
|
}
|
|
|
|
//validate checkout attributes
|
|
if (!validateCheckoutAttributes)
|
|
return warnings;
|
|
|
|
//selected attributes
|
|
var attributes1 = await _checkoutAttributeParser.ParseAttributesAsync(checkoutAttributesXml);
|
|
|
|
//existing checkout attributes
|
|
var excludeShippableAttributes = !await ShoppingCartRequiresShippingAsync(shoppingCart);
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var attributes2 = await _checkoutAttributeService.GetAllAttributesAsync(_staticCacheManager, _storeMappingService, store.Id, excludeShippableAttributes);
|
|
|
|
//validate conditional attributes only (if specified)
|
|
attributes2 = await attributes2.WhereAwait(async x =>
|
|
{
|
|
var conditionMet = await _checkoutAttributeParser.IsConditionMetAsync(x.ConditionAttributeXml, checkoutAttributesXml);
|
|
return !conditionMet.HasValue || conditionMet.Value;
|
|
}).ToListAsync();
|
|
|
|
foreach (var a2 in attributes2)
|
|
{
|
|
if (!a2.IsRequired)
|
|
continue;
|
|
|
|
var found = false;
|
|
//selected checkout attributes
|
|
foreach (var a1 in attributes1)
|
|
{
|
|
if (a1.Id != a2.Id)
|
|
continue;
|
|
|
|
var attributeValuesStr = _checkoutAttributeParser.ParseValues(checkoutAttributesXml, a1.Id);
|
|
foreach (var str1 in attributeValuesStr)
|
|
if (!string.IsNullOrEmpty(str1.Trim()))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found)
|
|
continue;
|
|
|
|
//if not found
|
|
warnings.Add(!string.IsNullOrEmpty(await _localizationService.GetLocalizedAsync(a2, a => a.TextPrompt))
|
|
? await _localizationService.GetLocalizedAsync(a2, a => a.TextPrompt)
|
|
: string.Format(await _localizationService.GetResourceAsync("ShoppingCart.SelectAttribute"),
|
|
await _localizationService.GetLocalizedAsync(a2, a => a.Name)));
|
|
}
|
|
|
|
//now validation rules
|
|
|
|
//minimum length
|
|
foreach (var ca in attributes2)
|
|
{
|
|
string enteredText;
|
|
int enteredTextLength;
|
|
|
|
if (ca.ValidationMinLength.HasValue)
|
|
{
|
|
if (ca.AttributeControlType == AttributeControlType.TextBox ||
|
|
ca.AttributeControlType == AttributeControlType.MultilineTextbox)
|
|
{
|
|
enteredText = _checkoutAttributeParser.ParseValues(checkoutAttributesXml, ca.Id).FirstOrDefault();
|
|
enteredTextLength = string.IsNullOrEmpty(enteredText) ? 0 : enteredText.Length;
|
|
|
|
if (ca.ValidationMinLength.Value > enteredTextLength)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.TextboxMinimumLength"), await _localizationService.GetLocalizedAsync(ca, a => a.Name), ca.ValidationMinLength.Value));
|
|
}
|
|
}
|
|
}
|
|
|
|
//maximum length
|
|
if (!ca.ValidationMaxLength.HasValue)
|
|
continue;
|
|
|
|
if (ca.AttributeControlType != AttributeControlType.TextBox && ca.AttributeControlType != AttributeControlType.MultilineTextbox)
|
|
continue;
|
|
|
|
enteredText = _checkoutAttributeParser.ParseValues(checkoutAttributesXml, ca.Id).FirstOrDefault();
|
|
enteredTextLength = string.IsNullOrEmpty(enteredText) ? 0 : enteredText.Length;
|
|
|
|
if (ca.ValidationMaxLength.Value < enteredTextLength)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.TextboxMaximumLength"), await _localizationService.GetLocalizedAsync(ca, a => a.Name), ca.ValidationMaxLength.Value));
|
|
}
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the shopping cart item sub total
|
|
/// </summary>
|
|
/// <param name="shoppingCartItem">The shopping cart item</param>
|
|
/// <param name="includeDiscounts">A value indicating whether include discounts or not for price computation</param>
|
|
/// <returns>Shopping cart item sub total. Applied discount amount. Applied discounts. Maximum discounted qty. Return not nullable value if discount cannot be applied to ALL items</returns>
|
|
public virtual async Task<(decimal subTotal, decimal discountAmount, List<Discount> appliedDiscounts, int? maximumDiscountQty)> GetSubTotalAsync(ShoppingCartItem shoppingCartItem,
|
|
bool includeDiscounts)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(shoppingCartItem);
|
|
|
|
decimal subTotal;
|
|
int? maximumDiscountQty = null;
|
|
|
|
//unit price
|
|
var (unitPrice, discountAmount, appliedDiscounts) = await GetUnitPriceAsync(shoppingCartItem, includeDiscounts);
|
|
|
|
//discount
|
|
if (appliedDiscounts.Any())
|
|
{
|
|
//we can properly use "MaximumDiscountedQuantity" property only for one discount (not cumulative ones)
|
|
Discount oneAndOnlyDiscount = null;
|
|
if (appliedDiscounts.Count == 1)
|
|
oneAndOnlyDiscount = appliedDiscounts.First();
|
|
|
|
if ((oneAndOnlyDiscount?.MaximumDiscountedQuantity.HasValue ?? false) &&
|
|
shoppingCartItem.Quantity > oneAndOnlyDiscount.MaximumDiscountedQuantity.Value)
|
|
{
|
|
maximumDiscountQty = oneAndOnlyDiscount.MaximumDiscountedQuantity.Value;
|
|
//we cannot apply discount for all shopping cart items
|
|
var discountedQuantity = oneAndOnlyDiscount.MaximumDiscountedQuantity.Value;
|
|
var discountedSubTotal = unitPrice * discountedQuantity;
|
|
discountAmount *= discountedQuantity;
|
|
|
|
var notDiscountedQuantity = shoppingCartItem.Quantity - discountedQuantity;
|
|
var notDiscountedUnitPrice = (await GetUnitPriceAsync(shoppingCartItem, false)).unitPrice;
|
|
var notDiscountedSubTotal = notDiscountedUnitPrice * notDiscountedQuantity;
|
|
|
|
subTotal = discountedSubTotal + notDiscountedSubTotal;
|
|
}
|
|
else
|
|
{
|
|
//discount is applied to all items (quantity)
|
|
//calculate discount amount for all items
|
|
discountAmount *= shoppingCartItem.Quantity;
|
|
|
|
subTotal = unitPrice * shoppingCartItem.Quantity;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
subTotal = unitPrice * shoppingCartItem.Quantity;
|
|
}
|
|
|
|
return (subTotal, discountAmount, appliedDiscounts, maximumDiscountQty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the shopping cart unit price (one item)
|
|
/// </summary>
|
|
/// <param name="shoppingCartItem">The shopping cart item</param>
|
|
/// <param name="includeDiscounts">A value indicating whether include discounts or not for price computation</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the shopping cart unit price (one item). Applied discount amount. Applied discounts
|
|
/// </returns>
|
|
public virtual async Task<(decimal unitPrice, decimal discountAmount, List<Discount> appliedDiscounts)> GetUnitPriceAsync(ShoppingCartItem shoppingCartItem,
|
|
bool includeDiscounts)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(shoppingCartItem);
|
|
|
|
var customer = await _customerService.GetCustomerByIdAsync(shoppingCartItem.CustomerId);
|
|
var product = await _productService.GetProductByIdAsync(shoppingCartItem.ProductId);
|
|
var store = await _storeService.GetStoreByIdAsync(shoppingCartItem.StoreId);
|
|
|
|
return await GetUnitPriceAsync(product,
|
|
customer,
|
|
store,
|
|
shoppingCartItem.ShoppingCartType,
|
|
shoppingCartItem.Quantity,
|
|
shoppingCartItem.AttributesXml,
|
|
shoppingCartItem.CustomerEnteredPrice,
|
|
shoppingCartItem.RentalStartDateUtc,
|
|
shoppingCartItem.RentalEndDateUtc,
|
|
includeDiscounts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the shopping cart unit price (one item)
|
|
/// </summary>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="store">Store</param>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="quantity">Quantity</param>
|
|
/// <param name="attributesXml">Product attributes (XML format)</param>
|
|
/// <param name="customerEnteredPrice">Customer entered price (if specified)</param>
|
|
/// <param name="rentalStartDate">Rental start date (null for not rental products)</param>
|
|
/// <param name="rentalEndDate">Rental end date (null for not rental products)</param>
|
|
/// <param name="includeDiscounts">A value indicating whether include discounts or not for price computation</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the shopping cart unit price (one item). Applied discount amount. Applied discounts
|
|
/// </returns>
|
|
public virtual async Task<(decimal unitPrice, decimal discountAmount, List<Discount> appliedDiscounts)> GetUnitPriceAsync(Product product,
|
|
Customer customer,
|
|
Store store,
|
|
ShoppingCartType shoppingCartType,
|
|
int quantity,
|
|
string attributesXml,
|
|
decimal customerEnteredPrice,
|
|
DateTime? rentalStartDate, DateTime? rentalEndDate,
|
|
bool includeDiscounts)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
ArgumentNullException.ThrowIfNull(customer);
|
|
|
|
var discountAmount = decimal.Zero;
|
|
var appliedDiscounts = new List<Discount>();
|
|
|
|
decimal finalPrice;
|
|
|
|
var combination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml);
|
|
if (combination?.OverriddenPrice.HasValue ?? false)
|
|
{
|
|
(_, finalPrice, discountAmount, appliedDiscounts) = await _priceCalculationService.GetFinalPriceAsync(product,
|
|
customer,
|
|
store,
|
|
combination.OverriddenPrice.Value,
|
|
decimal.Zero,
|
|
includeDiscounts,
|
|
quantity,
|
|
product.IsRental ? rentalStartDate : null,
|
|
product.IsRental ? rentalEndDate : null);
|
|
}
|
|
else
|
|
{
|
|
//summarize price of all attributes
|
|
var attributesTotalPrice = decimal.Zero;
|
|
var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(attributesXml);
|
|
if (attributeValues != null)
|
|
{
|
|
foreach (var attributeValue in attributeValues)
|
|
{
|
|
attributesTotalPrice += await _priceCalculationService.GetProductAttributeValuePriceAdjustmentAsync(product,
|
|
attributeValue,
|
|
customer,
|
|
store,
|
|
product.CustomerEntersPrice ? (decimal?)customerEnteredPrice : null,
|
|
quantity);
|
|
}
|
|
}
|
|
|
|
//get price of a product (with previously calculated price of all attributes)
|
|
if (product.CustomerEntersPrice)
|
|
{
|
|
finalPrice = customerEnteredPrice;
|
|
}
|
|
else
|
|
{
|
|
int qty;
|
|
if (_shoppingCartSettings.GroupTierPricesForDistinctShoppingCartItems)
|
|
{
|
|
//the same products with distinct product attributes could be stored as distinct "ShoppingCartItem" records
|
|
//so let's find how many of the current products are in the cart
|
|
qty = (await GetShoppingCartAsync(customer, shoppingCartType: shoppingCartType, productId: product.Id))
|
|
.Sum(x => x.Quantity);
|
|
|
|
if (qty == 0)
|
|
{
|
|
qty = quantity;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qty = quantity;
|
|
}
|
|
|
|
(_, finalPrice, discountAmount, appliedDiscounts) = await _priceCalculationService.GetFinalPriceAsync(product,
|
|
customer,
|
|
store,
|
|
attributesTotalPrice,
|
|
includeDiscounts,
|
|
qty,
|
|
product.IsRental ? rentalStartDate : null,
|
|
product.IsRental ? rentalEndDate : null);
|
|
}
|
|
}
|
|
|
|
//rounding
|
|
if (_shoppingCartSettings.RoundPricesDuringCalculation)
|
|
finalPrice = await _priceCalculationService.RoundPriceAsync(finalPrice);
|
|
|
|
return (finalPrice, discountAmount, appliedDiscounts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a shopping cart item in the cart
|
|
/// </summary>
|
|
/// <param name="shoppingCart">Shopping cart</param>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <param name="customerEnteredPrice">Price entered by a customer</param>
|
|
/// <param name="rentalStartDate">Rental start date</param>
|
|
/// <param name="rentalEndDate">Rental end date</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the found shopping cart item
|
|
/// </returns>
|
|
public virtual async Task<ShoppingCartItem> FindShoppingCartItemInTheCartAsync(IList<ShoppingCartItem> shoppingCart,
|
|
ShoppingCartType shoppingCartType,
|
|
Product product,
|
|
string attributesXml = "",
|
|
decimal customerEnteredPrice = decimal.Zero,
|
|
DateTime? rentalStartDate = null,
|
|
DateTime? rentalEndDate = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(shoppingCart);
|
|
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
return await shoppingCart.Where(sci => sci.ShoppingCartType == shoppingCartType)
|
|
.FirstOrDefaultAwaitAsync(async sci => await ShoppingCartItemIsEqualAsync(sci, product, attributesXml, customerEnteredPrice, rentalStartDate, rentalEndDate));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a product to shopping cart
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="product">Product</param>
|
|
/// <param name="shoppingCartType">Shopping cart type</param>
|
|
/// <param name="storeId">Store identifier</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <param name="customerEnteredPrice">The price enter by a customer</param>
|
|
/// <param name="rentalStartDate">Rental start date</param>
|
|
/// <param name="rentalEndDate">Rental end date</param>
|
|
/// <param name="quantity">Quantity</param>
|
|
/// <param name="addRequiredProducts">Whether to add required products</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
public virtual async Task<IList<string>> AddToCartAsync(Customer customer, Product product,
|
|
ShoppingCartType shoppingCartType, int storeId, string attributesXml = null,
|
|
decimal customerEnteredPrice = decimal.Zero,
|
|
DateTime? rentalStartDate = null, DateTime? rentalEndDate = null,
|
|
int quantity = 1, bool addRequiredProducts = true)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(customer);
|
|
|
|
ArgumentNullException.ThrowIfNull(product);
|
|
|
|
var warnings = new List<string>();
|
|
if (shoppingCartType == ShoppingCartType.ShoppingCart && !await _permissionService.AuthorizeAsync(StandardPermission.PublicStore.ENABLE_SHOPPING_CART, customer))
|
|
{
|
|
warnings.Add("Shopping cart is disabled");
|
|
return warnings;
|
|
}
|
|
|
|
if (shoppingCartType == ShoppingCartType.Wishlist && !await _permissionService.AuthorizeAsync(StandardPermission.PublicStore.ENABLE_WISHLIST, customer))
|
|
{
|
|
warnings.Add("Wishlist is disabled");
|
|
return warnings;
|
|
}
|
|
|
|
if (customer.IsSearchEngineAccount())
|
|
{
|
|
warnings.Add("Search engine can't add to cart");
|
|
return warnings;
|
|
}
|
|
|
|
if (quantity <= 0)
|
|
{
|
|
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.QuantityShouldPositive"));
|
|
return warnings;
|
|
}
|
|
|
|
//reset checkout info
|
|
await _customerService.ResetCheckoutDataAsync(customer, storeId);
|
|
|
|
var cart = await GetShoppingCartAsync(customer, shoppingCartType, storeId);
|
|
|
|
var shoppingCartItem = await FindShoppingCartItemInTheCartAsync(cart,
|
|
shoppingCartType, product, attributesXml, customerEnteredPrice,
|
|
rentalStartDate, rentalEndDate);
|
|
|
|
if (shoppingCartItem != null)
|
|
{
|
|
//update existing shopping cart item
|
|
var newQuantity = shoppingCartItem.Quantity + quantity;
|
|
|
|
await addRequiredProductsToCartAsync(newQuantity);
|
|
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
warnings.AddRange(await GetShoppingCartItemWarningsAsync(customer, shoppingCartType, product,
|
|
storeId, attributesXml,
|
|
customerEnteredPrice, rentalStartDate, rentalEndDate,
|
|
newQuantity, addRequiredProducts, shoppingCartItem.Id));
|
|
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
shoppingCartItem.AttributesXml = attributesXml;
|
|
shoppingCartItem.Quantity = newQuantity;
|
|
shoppingCartItem.UpdatedOnUtc = DateTime.UtcNow;
|
|
|
|
await _sciRepository.UpdateAsync(shoppingCartItem);
|
|
}
|
|
else
|
|
{
|
|
//new shopping cart item
|
|
warnings.AddRange(await GetShoppingCartItemWarningsAsync(customer, shoppingCartType, product,
|
|
storeId, attributesXml, customerEnteredPrice,
|
|
rentalStartDate, rentalEndDate,
|
|
quantity, addRequiredProducts));
|
|
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
await addRequiredProductsToCartAsync();
|
|
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
//maximum items validation
|
|
switch (shoppingCartType)
|
|
{
|
|
case ShoppingCartType.ShoppingCart:
|
|
if (cart.Count >= _shoppingCartSettings.MaximumShoppingCartItems)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.MaximumShoppingCartItems"), _shoppingCartSettings.MaximumShoppingCartItems));
|
|
return warnings;
|
|
}
|
|
|
|
break;
|
|
case ShoppingCartType.Wishlist:
|
|
if (cart.Count >= _shoppingCartSettings.MaximumWishlistItems)
|
|
{
|
|
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.MaximumWishlistItems"), _shoppingCartSettings.MaximumWishlistItems));
|
|
return warnings;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
shoppingCartItem = new ShoppingCartItem
|
|
{
|
|
ShoppingCartType = shoppingCartType,
|
|
StoreId = storeId,
|
|
ProductId = product.Id,
|
|
AttributesXml = attributesXml,
|
|
CustomerEnteredPrice = customerEnteredPrice,
|
|
Quantity = quantity,
|
|
RentalStartDateUtc = rentalStartDate,
|
|
RentalEndDateUtc = rentalEndDate,
|
|
CreatedOnUtc = now,
|
|
UpdatedOnUtc = now,
|
|
CustomerId = customer.Id
|
|
};
|
|
|
|
await _sciRepository.InsertAsync(shoppingCartItem);
|
|
|
|
//updated "HasShoppingCartItems" property used for performance optimization
|
|
var hasShoppingCartItems = !IsCustomerShoppingCartEmpty(customer);
|
|
if (hasShoppingCartItems != customer.HasShoppingCartItems)
|
|
{
|
|
customer.HasShoppingCartItems = hasShoppingCartItems;
|
|
await _customerService.UpdateCustomerAsync(customer);
|
|
}
|
|
}
|
|
|
|
return warnings;
|
|
|
|
async Task addRequiredProductsToCartAsync(int qty = 0)
|
|
{
|
|
if (!product.RequireOtherProducts)
|
|
return;
|
|
|
|
//get these required products
|
|
var requiredProducts = await _productService.GetProductsByIdsAsync(_productService.ParseRequiredProductIds(product));
|
|
if (!requiredProducts.Any())
|
|
return;
|
|
|
|
var finalRequiredProducts = requiredProducts.GroupBy(p => p.Id)
|
|
.Select(g => new { Product = g.First(), Count = g.Count() });
|
|
|
|
foreach (var requiredProduct in finalRequiredProducts)
|
|
{
|
|
var productsRequiringRequiredProduct = await GetProductsRequiringProductAsync(cart, requiredProduct.Product);
|
|
|
|
//get the required quantity of the required product
|
|
var requiredProductRequiredQuantity = (qty > 0 ? qty : quantity) +
|
|
cart.Where(ci => productsRequiringRequiredProduct.Any(p => p.Id == ci.ProductId))
|
|
.Where(item => item.Id != (shoppingCartItem?.Id ?? 0))
|
|
.Sum(item => item.Quantity);
|
|
|
|
//whether required product is already in the cart in the required quantity
|
|
var quantityToAdd = requiredProductRequiredQuantity * requiredProduct.Count - (cart.FirstOrDefault(item => item.ProductId == requiredProduct.Product.Id)?.Quantity ?? 0);
|
|
if (quantityToAdd <= 0)
|
|
continue;
|
|
|
|
if (addRequiredProducts && product.AutomaticallyAddRequiredProducts)
|
|
{
|
|
//do not add required products to prevent circular references
|
|
var addToCartWarnings = await AddToCartAsync(customer, requiredProduct.Product, shoppingCartType, storeId,
|
|
quantity: quantityToAdd, addRequiredProducts: requiredProduct.Product.AutomaticallyAddRequiredProducts);
|
|
|
|
if (addToCartWarnings.Any())
|
|
{
|
|
warnings.AddRange(addToCartWarnings);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the shopping cart item
|
|
/// </summary>
|
|
/// <param name="customer">Customer</param>
|
|
/// <param name="shoppingCartItemId">Shopping cart item identifier</param>
|
|
/// <param name="attributesXml">Attributes in XML format</param>
|
|
/// <param name="customerEnteredPrice">New customer entered price</param>
|
|
/// <param name="rentalStartDate">Rental start date</param>
|
|
/// <param name="rentalEndDate">Rental end date</param>
|
|
/// <param name="quantity">New shopping cart item quantity</param>
|
|
/// <param name="resetCheckoutData">A value indicating whether to reset checkout data</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the warnings
|
|
/// </returns>
|
|
public virtual async Task<IList<string>> UpdateShoppingCartItemAsync(Customer customer,
|
|
int shoppingCartItemId, string attributesXml,
|
|
decimal customerEnteredPrice,
|
|
DateTime? rentalStartDate = null, DateTime? rentalEndDate = null,
|
|
int quantity = 1, bool resetCheckoutData = true)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(customer);
|
|
|
|
var warnings = new List<string>();
|
|
|
|
var shoppingCartItem = await _sciRepository.GetByIdAsync(shoppingCartItemId, cache => default);
|
|
|
|
if (shoppingCartItem == null || shoppingCartItem.CustomerId != customer.Id)
|
|
return warnings;
|
|
|
|
if (resetCheckoutData)
|
|
{
|
|
//reset checkout data
|
|
await _customerService.ResetCheckoutDataAsync(customer, shoppingCartItem.StoreId);
|
|
}
|
|
|
|
var product = await _productService.GetProductByIdAsync(shoppingCartItem.ProductId);
|
|
|
|
if (quantity > 0)
|
|
{
|
|
//check warnings
|
|
warnings.AddRange(await GetShoppingCartItemWarningsAsync(customer, shoppingCartItem.ShoppingCartType,
|
|
product, shoppingCartItem.StoreId,
|
|
attributesXml, customerEnteredPrice,
|
|
rentalStartDate, rentalEndDate, quantity, false, shoppingCartItemId));
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
//if everything is OK, then update a shopping cart item
|
|
shoppingCartItem.Quantity = quantity;
|
|
shoppingCartItem.AttributesXml = attributesXml;
|
|
shoppingCartItem.CustomerEnteredPrice = customerEnteredPrice;
|
|
shoppingCartItem.RentalStartDateUtc = rentalStartDate;
|
|
shoppingCartItem.RentalEndDateUtc = rentalEndDate;
|
|
shoppingCartItem.UpdatedOnUtc = DateTime.UtcNow;
|
|
|
|
await _sciRepository.UpdateAsync(shoppingCartItem);
|
|
}
|
|
else
|
|
{
|
|
//check warnings for required products
|
|
warnings.AddRange(await GetRequiredProductWarningsAsync(customer, shoppingCartItem.ShoppingCartType,
|
|
product, shoppingCartItem.StoreId, quantity, false, shoppingCartItemId));
|
|
if (warnings.Any())
|
|
return warnings;
|
|
|
|
//delete a shopping cart item
|
|
await DeleteShoppingCartItemAsync(shoppingCartItem, resetCheckoutData, true);
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Migrate shopping cart
|
|
/// </summary>
|
|
/// <param name="fromCustomer">From customer</param>
|
|
/// <param name="toCustomer">To customer</param>
|
|
/// <param name="includeCouponCodes">A value indicating whether to coupon codes (discount and gift card) should be also re-applied</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public virtual async Task MigrateShoppingCartAsync(Customer fromCustomer, Customer toCustomer, bool includeCouponCodes)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(fromCustomer);
|
|
ArgumentNullException.ThrowIfNull(toCustomer);
|
|
|
|
if (fromCustomer.Id == toCustomer.Id)
|
|
return; //the same customer
|
|
|
|
//shopping cart items
|
|
var fromCart = await GetShoppingCartAsync(fromCustomer);
|
|
|
|
for (var i = 0; i < fromCart.Count; i++)
|
|
{
|
|
var sci = fromCart[i];
|
|
var product = await _productService.GetProductByIdAsync(sci.ProductId);
|
|
|
|
await AddToCartAsync(toCustomer, product, sci.ShoppingCartType, sci.StoreId,
|
|
sci.AttributesXml, sci.CustomerEnteredPrice,
|
|
sci.RentalStartDateUtc, sci.RentalEndDateUtc, sci.Quantity, false);
|
|
}
|
|
|
|
for (var i = 0; i < fromCart.Count; i++)
|
|
{
|
|
var sci = fromCart[i];
|
|
await DeleteShoppingCartItemAsync(sci);
|
|
}
|
|
|
|
//copy discount and gift card coupon codes
|
|
if (includeCouponCodes)
|
|
{
|
|
//discount
|
|
foreach (var code in await _customerService.ParseAppliedDiscountCouponCodesAsync(fromCustomer))
|
|
await _customerService.ApplyDiscountCouponCodeAsync(toCustomer, code);
|
|
|
|
//gift card
|
|
foreach (var code in await _customerService.ParseAppliedGiftCardCouponCodesAsync(fromCustomer))
|
|
await _customerService.ApplyGiftCardCouponCodeAsync(toCustomer, code);
|
|
}
|
|
|
|
//move selected checkout attributes
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var checkoutAttributesXml = await _genericAttributeService.GetAttributeAsync<string>(fromCustomer, NopCustomerDefaults.CheckoutAttributes, store.Id);
|
|
await _genericAttributeService.SaveAttributeAsync(toCustomer, NopCustomerDefaults.CheckoutAttributes, checkoutAttributesXml, store.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates whether the shopping cart requires shipping
|
|
/// </summary>
|
|
/// <param name="shoppingCart">Shopping cart</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains true if the shopping cart requires shipping; otherwise, false.
|
|
/// </returns>
|
|
public virtual async Task<bool> ShoppingCartRequiresShippingAsync(IList<ShoppingCartItem> shoppingCart)
|
|
{
|
|
return await shoppingCart.AnyAwaitAsync(async shoppingCartItem => await _shippingService.IsShipEnabledAsync(shoppingCartItem));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether shopping cart is recurring
|
|
/// </summary>
|
|
/// <param name="shoppingCart">Shopping cart</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the result
|
|
/// </returns>
|
|
public virtual async Task<bool> ShoppingCartIsRecurringAsync(IList<ShoppingCartItem> shoppingCart)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(shoppingCart);
|
|
|
|
if (!shoppingCart.Any())
|
|
return false;
|
|
|
|
return await _productService.HasAnyRecurringProductAsync(shoppingCart.Select(sci => sci.ProductId).ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a recurring cycle information
|
|
/// </summary>
|
|
/// <param name="shoppingCart">Shopping cart</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the error (if exists); otherwise, empty string. Cycle length. Cycle period. Total cycles
|
|
/// </returns>
|
|
public virtual async Task<(string error, int cycleLength, RecurringProductCyclePeriod cyclePeriod, int totalCycles)> GetRecurringCycleInfoAsync(IList<ShoppingCartItem> shoppingCart)
|
|
{
|
|
var rezCycleLength = 0;
|
|
RecurringProductCyclePeriod rezCyclePeriod = 0;
|
|
var rezTotalCycles = 0;
|
|
|
|
int? cycleLength = null;
|
|
RecurringProductCyclePeriod? cyclePeriod = null;
|
|
int? totalCycles = null;
|
|
|
|
var conflictError = await _localizationService.GetResourceAsync("ShoppingCart.ConflictingShipmentSchedules");
|
|
|
|
foreach (var sci in shoppingCart)
|
|
{
|
|
var product = await _productService.GetProductByIdAsync(sci.ProductId) ?? throw new NopException($"Product (Id={sci.ProductId}) cannot be loaded");
|
|
|
|
if (!product.IsRecurring)
|
|
continue;
|
|
|
|
//cycle length
|
|
if (cycleLength.HasValue && cycleLength.Value != product.RecurringCycleLength)
|
|
return (conflictError, rezCycleLength, rezCyclePeriod, rezTotalCycles);
|
|
cycleLength = product.RecurringCycleLength;
|
|
|
|
//cycle period
|
|
if (cyclePeriod.HasValue && cyclePeriod.Value != product.RecurringCyclePeriod)
|
|
return (conflictError, rezCycleLength, rezCyclePeriod, rezTotalCycles);
|
|
cyclePeriod = product.RecurringCyclePeriod;
|
|
|
|
//total cycles
|
|
if (totalCycles.HasValue && totalCycles.Value != product.RecurringTotalCycles)
|
|
return (conflictError, rezCycleLength, rezCyclePeriod, rezTotalCycles);
|
|
totalCycles = product.RecurringTotalCycles;
|
|
}
|
|
|
|
if (!cycleLength.HasValue)
|
|
return (string.Empty, rezCycleLength, rezCyclePeriod, rezTotalCycles);
|
|
|
|
rezCycleLength = cycleLength.Value;
|
|
rezCyclePeriod = cyclePeriod.Value;
|
|
rezTotalCycles = totalCycles.Value;
|
|
|
|
return (string.Empty, rezCycleLength, rezCyclePeriod, rezTotalCycles);
|
|
}
|
|
|
|
#endregion
|
|
} |