// TODO: (ms) (core) Test this for bundle & grouped products. public virtual async Task <bool> AddToCartAsync(AddToCartContext ctx) { Guard.NotNull(ctx, nameof(ctx)); // This is called when customer adds a product to cart ctx.Customer ??= _workContext.CurrentCustomer; ctx.StoreId ??= _storeContext.CurrentStore.Id; ctx.Customer.ResetCheckoutData(ctx.StoreId.Value); // Checks whether attributes have been selected if (ctx.VariantQuery != null || ctx.RawAttributes.HasValue()) { if (!ctx.RawAttributes.HasValue()) { await _db.LoadCollectionAsync(ctx.Product, x => x.ProductVariantAttributes, false); var(Selection, Warnings) = await _productAttributeMaterializer.CreateAttributeSelectionAsync( ctx.VariantQuery, ctx.Product.ProductVariantAttributes, ctx.Product.Id, ctx.BundleItemId); ctx.RawAttributes = Selection.AttributesMap.Any() ? Selection.AsJson() : string.Empty; } // Check context for bundle item errors if (ctx.Product.ProductType == ProductType.BundledProduct && ctx.RawAttributes.HasValue()) { ctx.Warnings.Add(T("ShoppingCart.Bundle.NoAttributes")); if (ctx.BundleItem != null) { return(false); } } } if (!await _cartValidator.ValidateAccessPermissionsAsync(ctx.Customer, ctx.CartType, ctx.Warnings)) { return(false); } var cartItems = await GetCartItemsAsync(ctx.Customer, ctx.CartType, ctx.StoreId.Value); // Adds required products automatically if it is enabled if (ctx.AutomaticallyAddRequiredProducts) { var requiredProductIds = ctx.Product.ParseRequiredProductIds(); if (requiredProductIds.Any()) { var cartProductIds = cartItems.Select(x => x.Item.ProductId); var missingRequiredProductIds = requiredProductIds.Except(cartProductIds); var missingRequiredProducts = await _db.Products.GetManyAsync(missingRequiredProductIds, false); foreach (var product in missingRequiredProducts) { var item = new ShoppingCartItem { CustomerEnteredPrice = ctx.CustomerEnteredPrice.Amount, RawAttributes = ctx.AttributeSelection.AsJson(), ShoppingCartType = ctx.CartType, StoreId = ctx.StoreId.Value, Quantity = ctx.Quantity, Customer = ctx.Customer, Product = product, BundleItemId = ctx.BundleItem?.Id }; await AddItemToCartAsync(new AddToCartContext { Item = item, ChildItems = ctx.ChildItems, Customer = ctx.Customer }); } } } // Checks whether required products are still missing await _cartValidator.ValidateRequiredProductsAsync(ctx.Product, cartItems, ctx.Warnings); ShoppingCartItem existingCartItem = null; if (ctx.BundleItem == null) { existingCartItem = cartItems.FindItemInCart(ctx.CartType, ctx.Product, ctx.AttributeSelection, ctx.CustomerEnteredPrice)?.Item; } // Add item to cart (if no warnings accured) if (existingCartItem != null) { // Product is already in cart, find existing item var newQuantity = ctx.Quantity + existingCartItem.Quantity; if (!await _cartValidator.ValidateAddToCartItemAsync(ctx, existingCartItem, cartItems)) { return(false); } // Update cart item existingCartItem.Quantity = newQuantity; existingCartItem.UpdatedOnUtc = DateTime.UtcNow; existingCartItem.RawAttributes = ctx.AttributeSelection.AsJson(); await _db.SaveChangesAsync(); return(true); } else { if (!_cartValidator.ValidateItemsMaximumCartQuantity(ctx.CartType, cartItems.Count, ctx.Warnings)) { return(false); } // Product is not in cart yet, create new item var cartItem = new ShoppingCartItem { CustomerEnteredPrice = ctx.CustomerEnteredPrice.Amount, RawAttributes = ctx.RawAttributes, ShoppingCartType = ctx.CartType, StoreId = ctx.StoreId.Value, Quantity = ctx.Quantity, Customer = ctx.Customer, Product = ctx.Product, ProductId = ctx.Product.Id, ParentItemId = null, BundleItemId = ctx.BundleItem?.Id, BundleItem = ctx.BundleItem }; // TODO: (core) (ms) Fix bundle attributes of child items & customer selected price if (!await _cartValidator.ValidateAddToCartItemAsync(ctx, cartItem, cartItems)) { return(false); } // Checks whether the product is the parent item of a bundle, or just a simple product. if (ctx.BundleItem == null) { // Set cart item as item for simple & bundle products, only if its not set by the caller ctx.Item ??= cartItem; } else { // Add item as child of bundle ctx.ChildItems.Add(cartItem); } } _requestCache.RemoveByPattern(CartItemsPatternKey); // If ctx.Product is a bundle product and the setting to automatically add bundle products is true, try to add all corresponding BundleItems. if (ctx.AutomaticallyAddBundleProducts && ctx.Product.ProductType == ProductType.BundledProduct && ctx.BundleItem == null && ctx.Warnings.Count == 0) { var bundleItems = await _db.ProductBundleItem .ApplyBundledProductsFilter(new[] { ctx.Product.Id }, true) .Include(x => x.Product) .ToListAsync(); foreach (var bundleItem in bundleItems) { bundleItem.BundleProduct = ctx.Item.Product; var bundleItemContext = new AddToCartContext { StoreId = ctx.StoreId, Customer = ctx.Customer, CartType = ctx.CartType, BundleItem = bundleItem, ChildItems = ctx.ChildItems, Product = bundleItem.Product, Quantity = bundleItem.Quantity, VariantQuery = ctx.VariantQuery, RawAttributes = ctx.RawAttributes, CustomerEnteredPrice = ctx.CustomerEnteredPrice, AutomaticallyAddRequiredProducts = ctx.AutomaticallyAddRequiredProducts, }; if (!await AddToCartAsync(bundleItemContext)) { ctx.Warnings.AddRange(bundleItemContext.Warnings); } } } // Add item and its children (if active) to the cart, when it is either a simple product or // if it is the parent item of its bundle (bundleItem = null) and no warnings occurred. if (ctx.BundleItem == null && ctx.Warnings.Count == 0) { await AddItemToCartAsync(ctx); } return(!ctx.Warnings.Any()); }
protected virtual async Task <decimal> GetPreselectedPriceAmountAsync( Product product, Customer customer, PriceCalculationContext context, ProductBundleItemData bundleItem, IEnumerable <ProductBundleItemData> bundleItems) { var attributesTotalPriceBase = decimal.Zero; var preSelectedPriceAdjustmentBase = decimal.Zero; var isBundle = product.ProductType == ProductType.BundledProduct; var isBundleItemPricing = bundleItem?.Item?.BundleProduct?.BundlePerItemPricing ?? false; var isBundlePricing = bundleItem != null && !bundleItem.Item.BundleProduct.BundlePerItemPricing; var bundleItemId = bundleItem?.Item?.Id ?? 0; var query = new ProductVariantQuery(); var selectedAttributeValues = new List <ProductVariantAttributeValue>(); var attributes = await context.Attributes.GetOrLoadAsync(product.Id); // 1. Fill query with initially selected attributes. foreach (var attribute in attributes.Where(x => x.ProductVariantAttributeValues.Any() && x.IsListTypeAttribute())) { await _db.LoadCollectionAsync(attribute, x => x.ProductVariantAttributeValues); var preSelectedValueId = 0; var selectedValueIds = new List <int>(); ProductVariantAttributeValue defaultValue = null; var pvaValues = attribute.ProductVariantAttributeValues; foreach (var pvaValue in pvaValues) { ProductBundleItemAttributeFilter attributeFilter = null; if (bundleItem?.Item?.IsFilteredOut(pvaValue, out attributeFilter) ?? false) { continue; } if (preSelectedValueId == 0 && attributeFilter != null && attributeFilter.IsPreSelected) { preSelectedValueId = attributeFilter.AttributeValueId; } if (!isBundlePricing && pvaValue.IsPreSelected) { var attributeValuePriceAdjustment = await GetVariantPriceAdjustmentAsync(pvaValue, product, customer, context, 1); // We cannot avoid money usage in calls between interfaces. var(priceAdjustmentBase, _) = await _taxService.GetProductPriceAsync(product, new(attributeValuePriceAdjustment, _primaryCurrency), customer : customer); preSelectedPriceAdjustmentBase += priceAdjustmentBase.Amount; } } // Value pre-selected by a bundle item filter discards the default pre-selection. if (preSelectedValueId != 0 && (defaultValue = pvaValues.FirstOrDefault(x => x.Id == preSelectedValueId)) != null) { //defaultValue.IsPreSelected = true; selectedAttributeValues.Add(defaultValue); query.AddVariant(new ProductVariantQueryItem(defaultValue.Id.ToString()) { ProductId = product.Id, BundleItemId = bundleItemId, AttributeId = attribute.ProductAttributeId, VariantAttributeId = attribute.Id, Alias = attribute.ProductAttribute.Alias, ValueAlias = defaultValue.Alias }); } else { foreach (var value in pvaValues.Where(x => x.IsPreSelected)) { selectedAttributeValues.Add(value); query.AddVariant(new ProductVariantQueryItem(value.Id.ToString()) { ProductId = product.Id, BundleItemId = bundleItemId, AttributeId = attribute.ProductAttributeId, VariantAttributeId = attribute.Id, Alias = attribute.ProductAttribute.Alias, ValueAlias = value.Alias }); } } } // 2. Find attribute combination for selected attributes and merge it. if (!isBundle && query.Variants.Any()) { var(selection, warnings) = await _productAttributeMaterializer.CreateAttributeSelectionAsync(query, attributes, product.Id, bundleItemId, true); var combinations = await context.AttributeCombinations.GetOrLoadAsync(product.Id); var selectedCombination = combinations.FirstOrDefault(x => x.AttributeSelection.Equals(selection)); if (selectedCombination != null && selectedCombination.IsActive && selectedCombination.Price.HasValue) { product.MergedDataValues = new Dictionary <string, object> { { "Price", selectedCombination.Price.Value } }; if (selectedCombination.BasePriceAmount.HasValue) { product.MergedDataValues.Add("BasePriceAmount", selectedCombination.BasePriceAmount.Value); } if (selectedCombination.BasePriceBaseAmount.HasValue) { product.MergedDataValues.Add("BasePriceBaseAmount", selectedCombination.BasePriceBaseAmount.Value); } } } if (_catalogSettings.EnableDynamicPriceUpdate && !isBundlePricing) { if (selectedAttributeValues.Count > 0) { foreach (var value in selectedAttributeValues) { attributesTotalPriceBase += await GetVariantPriceAdjustmentAsync(value, product, customer, context, 1); } } else { attributesTotalPriceBase = preSelectedPriceAdjustmentBase; } } if (bundleItem != null) { bundleItem.AdditionalCharge = new(attributesTotalPriceBase, _primaryCurrency); } var result = await GetFinalPriceAmountAsync(product, bundleItems, attributesTotalPriceBase, customer, true, 1, bundleItem, context); return(result); }
// TODO: (ms) (core) TESTING! Make sure it works in any case - Works for ReOrder(). // TODO: (ms) (core) Test this for bundle & grouped products. Test for items with variants! public virtual async Task <bool> AddToCartAsync(AddToCartContext ctx) { Guard.NotNull(ctx, nameof(ctx)); // This is called when customer adds a product to cart ctx.Customer ??= _workContext.CurrentCustomer; ctx.StoreId ??= _storeContext.CurrentStore.Id; ctx.Customer.ResetCheckoutData(ctx.StoreId.Value); // Checks whether attributes have been selected if (ctx.VariantQuery != null) { // TODO: (ms) (core) fix wrong porting of attribute selection processing in AddToCartAsync. // Use _productAttributeMaterializer.CreateAttributeSelectionAsync to process them in context of VariantQuery. var attributes = await _db.ProductVariantAttributes //.Include(x => x.ProductAttribute) .ApplyProductFilter(new[] { ctx.Product.Id }) .ToListAsync(); var attributeSelection = await _productAttributeMaterializer.CreateAttributeSelectionAsync( ctx.VariantQuery, attributes, ctx.Product.Id, ctx.BundleItemId); ctx.RawAttributes = attributeSelection.Selection.AsJson(); // Check context for bundle item errors if (ctx.Product.ProductType == ProductType.BundledProduct && ctx.RawAttributes.HasValue()) { ctx.Warnings.Add(T("ShoppingCart.Bundle.NoAttributes")); if (ctx.BundleItem != null) { return(false); } } } if (!await _cartValidator.ValidateAccessPermissionsAsync(ctx.Customer, ctx.CartType, ctx.Warnings)) { return(false); } var cartItems = await GetCartItemsAsync(ctx.Customer, ctx.CartType, ctx.StoreId.Value); // Adds required products automatically if it is enabled if (ctx.AutomaticallyAddRequiredProducts) { var requiredProductIds = ctx.Product.ParseRequiredProductIds(); if (requiredProductIds.Any()) { var cartProductIds = cartItems.Select(x => x.Item.ProductId); var missingRequiredProductIds = requiredProductIds.Except(cartProductIds); var missingRequiredProducts = await _db.Products.GetManyAsync(missingRequiredProductIds); foreach (var product in missingRequiredProducts) { var item = new ShoppingCartItem { CustomerEnteredPrice = ctx.CustomerEnteredPrice.Amount, RawAttributes = ctx.AttributeSelection.AsJson(), ShoppingCartType = ctx.CartType, StoreId = ctx.StoreId.Value, Quantity = ctx.Quantity, Customer = ctx.Customer, Product = product, ParentItemId = product.ParentGroupedProductId, BundleItemId = ctx.BundleItem?.Id }; await AddItemToCartAsync(new AddToCartContext { Item = item, ChildItems = ctx.ChildItems, Customer = ctx.Customer }); } } } // Checks whether required products are still missing await _cartValidator.ValidateRequiredProductsAsync(ctx.Product, cartItems, ctx.Warnings); ShoppingCartItem existingCartItem = null; if (ctx.BundleItem == null) { existingCartItem = cartItems.FindItemInCart(ctx.CartType, ctx.Product, ctx.AttributeSelection, ctx.CustomerEnteredPrice)?.Item; } // Add item to cart (if no warnings accured) if (existingCartItem != null) { // Product is already in cart, find existing item var newQuantity = ctx.Quantity + existingCartItem.Quantity; if (!await _cartValidator.ValidateAddToCartItemAsync(ctx, existingCartItem, cartItems)) { return(false); } // Update cart item existingCartItem.Quantity = newQuantity; existingCartItem.UpdatedOnUtc = DateTime.UtcNow; existingCartItem.RawAttributes = ctx.AttributeSelection.AsJson(); _db.TryUpdate(ctx.Customer); await _db.SaveChangesAsync(); } else { if (!_cartValidator.ValidateItemsMaximumCartQuantity(ctx.CartType, cartItems.Count, ctx.Warnings)) { return(false); } // Product is not in cart yet, create new item var cartItem = new ShoppingCartItem { CustomerEnteredPrice = ctx.CustomerEnteredPrice.Amount, RawAttributes = ctx.RawAttributes, ShoppingCartType = ctx.CartType, StoreId = ctx.StoreId.Value, Quantity = ctx.Quantity, Customer = ctx.Customer, Product = ctx.Product, ProductId = ctx.Product.Id, ParentItemId = null, BundleItemId = ctx.BundleItem?.Id }; if (!await _cartValidator.ValidateAddToCartItemAsync(ctx, cartItem, cartItems)) { return(false); } // Check whether the product is part of a bundle, the bundle item or just any item. // If product is no child of bundle or no bundle at all if (ctx.BundleItem == null) { // Set cart item as item for simple & bundle products, only if its not set by the caller ctx.Item ??= cartItem; } else { ctx.ChildItems.Add(cartItem); } } _requestCache.RemoveByPattern(CartItemsPatternKey); // If ctx.Product is a bundle product and the setting to automatically add bundle products is true, try to add all corresponding BundleItems. if (ctx.AutomaticallyAddBundleProducts && ctx.Product.ProductType == ProductType.BundledProduct && ctx.BundleItem == null && ctx.Warnings.Count == 0) { var bundleItems = await _db.ProductBundleItem .Include(x => x.Product) .Include(x => x.BundleProduct) .ApplyBundledProductsFilter(new[] { ctx.Product.Id }, true) .ToListAsync(); foreach (var bundleItem in bundleItems) { var bundleItemContext = new AddToCartContext { Warnings = new(), Item = ctx.Item, StoreId = ctx.StoreId, Customer = ctx.Customer, CartType = ctx.CartType, BundleItem = bundleItem, ChildItems = ctx.ChildItems, Product = bundleItem.Product, Quantity = bundleItem.Quantity, VariantQuery = ctx.VariantQuery, RawAttributes = ctx.RawAttributes, CustomerEnteredPrice = ctx.CustomerEnteredPrice, AutomaticallyAddRequiredProducts = ctx.AutomaticallyAddRequiredProducts, }; // If bundleItem could not be added to the shopping cart, remove child items if (!await AddToCartAsync(bundleItemContext)) { ctx.ChildItems.Clear(); // TODO: (ms) (core) Add warning for bundle products that are unable to be added to the cart. break; } } } // If context is no bundleItem, add item (parent) and its children (grouped product) if ((ctx.Product.ProductType == ProductType.SimpleProduct || ctx.AutomaticallyAddBundleProducts) && ctx.BundleItem == null && ctx.Warnings.Count == 0) { await AddItemToCartAsync(ctx); } return(true); }
public async Task CalculateAsync(CalculatorContext context, CalculatorDelegate next) { var options = context.Options; if (!options.DeterminePreselectedPrice) { // Proceed with pipeline and omit this calculator, it is made for preselected price calculation only. await next(context); return; } var selectedValues = (await context.GetPreSelectedAttributeValuesAsync()) .Where(x => x.ProductVariantAttribute.IsListTypeAttribute()) .ToList(); if (selectedValues.Any()) { // Create attribute selection of preselected values. var query = new ProductVariantQuery(); var product = context.Product; var bundleItemId = context.BundleItem?.Item?.Id ?? 0; var attributes = await options.BatchContext.Attributes.GetOrLoadAsync(product.Id); var combinations = await options.BatchContext.AttributeCombinations.GetOrLoadAsync(product.Id); foreach (var value in selectedValues) { var productAttribute = value.ProductVariantAttribute; query.AddVariant(new ProductVariantQueryItem(value.Id.ToString()) { ProductId = product.Id, BundleItemId = bundleItemId, AttributeId = productAttribute.ProductAttributeId, VariantAttributeId = productAttribute.Id, Alias = productAttribute.ProductAttribute.Alias, ValueAlias = value.Alias }); } var(selection, _) = await _productAttributeMaterializer.CreateAttributeSelectionAsync(query, attributes, product.Id, bundleItemId, false); var selectedCombination = combinations.FirstOrDefault(x => x.AttributeSelection.Equals(selection)); // Apply attribute combination price. if ((selectedCombination?.IsActive ?? false) && selectedCombination.Price.HasValue) { context.FinalPrice = selectedCombination.Price.Value; // That comes too late because regular price has already been passed to child CalculatorContext: //product.MergedDataValues = new Dictionary<string, object> { { "Price", selectedCombination.Price.Value } }; } } // The product page is always loaded with the default quantity of 1. context.Quantity = 1; await next(context); context.PreselectedPrice = context.FinalPrice; }