Try your search with a different keyword or use * as a wildcard.
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 async Task<bool> IsCustomerShoppingCartEmptyAsync(Customer customer)
{
return !await _sciRepository.Table.AnyAsync(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.AgeVerification && product.MinimumAgeToPurchase > 0)
{
if (!customer.DateOfBirth.HasValue)
warnings.Add(await _localizationService.GetResourceAsync("ShoppingCart.DateOfBirthRequired"));
else if (CommonHelper.GetDifferenceInYears(customer.DateOfBirth.Value, DateTime.Today) < product.MinimumAgeToPurchase)
warnings.Add(string.Format(await _localizationService.GetResourceAsync("ShoppingCart.MinimumAgeToPurchase"), product.MinimumAgeToPurchase));
}
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 = !await IsCustomerShoppingCartEmptyAsync(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 = !await IsCustomerShoppingCartEmptyAsync(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>
/// <param name="customWishlistId">Custom wishlist identifier; pass 0 to load all records from all wishlists, pass null to load records from the default wishlist</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, int? customWishlistId = 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 by custom wishlist
if ((!shoppingCartType.HasValue || shoppingCartType == ShoppingCartType.Wishlist) && (customWishlistId is null || customWishlistId > 0))
items = items.Where(item => item.CustomWishlistId == customWishlistId);
//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);
//allow third-party handlers to select the unit price
var unitPriceEvent = new GetShoppingCartItemUnitPriceEvent(shoppingCartItem, includeDiscounts);
await _eventPublisher.PublishAsync(unitPriceEvent);
if (unitPriceEvent.StopProcessing)
return (unitPriceEvent.UnitPrice, unitPriceEvent.DiscountAmount, unitPriceEvent.AppliedDiscounts);
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>
/// <param name="wishlistId">Wishlist identifier; pass null if it's default wishlist</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, int? wishlistId = null)
{
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, wishlistId);
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(wishlistId: wishlistId);
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,
CustomWishlistId = shoppingCartType == ShoppingCartType.Wishlist ? wishlistId : null,
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 = !await IsCustomerShoppingCartEmptyAsync(customer);
if (hasShoppingCartItems != customer.HasShoppingCartItems)
{
customer.HasShoppingCartItems = hasShoppingCartItems;
await _customerService.UpdateCustomerAsync(customer);
}
}
return warnings;
async Task addRequiredProductsToCartAsync(int qty = 0, int? wishlistId = null)
{
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, wishlistId: wishlistId);
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>
/// Move shopping cart item to a custom wishlist
/// </summary>
/// <param name="shoppingCartItemId">Shopping cart item identifier</param>
/// <param name="wishlistId">Custom wishlist identifier</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task MoveItemToCustomWishlistAsync(int shoppingCartItemId, int? wishlistId = null)
{
var shoppingCartItemFrom = await _sciRepository.GetByIdAsync(shoppingCartItemId, cache => default);
if (shoppingCartItemFrom == null)
return;
var customer = await _customerService.GetCustomerByIdAsync(shoppingCartItemFrom.CustomerId);
var product = await _productService.GetProductByIdAsync(shoppingCartItemFrom.ProductId);
var cart = await GetShoppingCartAsync(customer, shoppingCartItemFrom.ShoppingCartType, shoppingCartItemFrom.StoreId, product.Id, customWishlistId: wishlistId);
var shoppingCartItemTo = await cart.FirstOrDefaultAwaitAsync(async sci => await ShoppingCartItemIsEqualAsync(sci, product, shoppingCartItemFrom.AttributesXml, shoppingCartItemFrom.CustomerEnteredPrice, shoppingCartItemFrom.RentalStartDateUtc, shoppingCartItemFrom.RentalEndDateUtc));
if (shoppingCartItemTo != null)
{
//update existing shopping cart item
var newQuantity = shoppingCartItemTo.Quantity + shoppingCartItemFrom.Quantity;
shoppingCartItemTo.Quantity = newQuantity;
shoppingCartItemTo.UpdatedOnUtc = DateTime.UtcNow;
await _sciRepository.UpdateAsync(shoppingCartItemTo);
await _sciRepository.DeleteAsync(shoppingCartItemFrom);
}
else
{
//update custom wishlist id
shoppingCartItemFrom.CustomWishlistId = wishlistId;
shoppingCartItemFrom.UpdatedOnUtc = DateTime.UtcNow;
await _sciRepository.UpdateAsync(shoppingCartItemFrom);
}
}
/// <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
}