Beispiel #1
0
        private async Task <ImageModel> PrepareOrderItemImageModelAsync(
            Product product,
            int pictureSize,
            string productName,
            ProductVariantAttributeSelection attributeSelection,
            CatalogSettings catalogSettings)
        {
            Guard.NotNull(product, nameof(product));

            MediaFileInfo file        = null;
            var           combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, attributeSelection);

            if (combination != null)
            {
                var mediaIds = combination.GetAssignedMediaIds();
                if (mediaIds.Any())
                {
                    file = await _mediaService.GetFileByIdAsync(mediaIds[0], MediaLoadFlags.AsNoTracking);
                }
            }

            // No attribute combination image, then load product picture.
            if (file == null)
            {
                var mediaFile = await _db.ProductMediaFiles
                                .AsNoTracking()
                                .Include(x => x.MediaFile)
                                .ApplyProductFilter(product.Id)
                                .FirstOrDefaultAsync();

                if (mediaFile?.MediaFile != null)
                {
                    file = _mediaService.ConvertMediaFile(mediaFile.MediaFile);
                }
            }

            // Let's check whether this product has some parent "grouped" product.
            if (file == null && product.Visibility == ProductVisibility.Hidden && product.ParentGroupedProductId > 0)
            {
                var mediaFile = await _db.ProductMediaFiles
                                .AsNoTracking()
                                .Include(x => x.MediaFile)
                                .ApplyProductFilter(product.ParentGroupedProductId)
                                .FirstOrDefaultAsync();

                if (mediaFile?.MediaFile != null)
                {
                    file = _mediaService.ConvertMediaFile(mediaFile.MediaFile);
                }
            }

            return(new ImageModel
            {
                File = file,
                ThumbSize = pictureSize,
                Title = file?.File?.GetLocalized(x => x.Title)?.Value.NullEmpty() ?? T("Media.Product.ImageLinkTitleFormat", productName),
                Alt = file?.File?.GetLocalized(x => x.Alt)?.Value.NullEmpty() ?? T("Media.Product.ImageAlternateTextFormat", productName),
                NoFallback = catalogSettings.HideProductDefaultPictures
            });
        }
Beispiel #2
0
        /// <summary>
        /// Finds and returns first matching product from shopping cart.
        /// </summary>
        /// <remarks>
        /// Products with the same identifier need to have matching attribute selections as well.
        /// </remarks>
        /// <param name="cart">Shopping cart to search in.</param>
        /// <param name="shoppingCartType">Shopping cart type to search in.</param>
        /// <param name="product">Product to search for.</param>
        /// <param name="selection">Attribute selection.</param>
        /// <param name="customerEnteredPrice">Customers entered price needs to match (if enabled by product).</param>
        /// <returns>Matching <see cref="OrganizedShoppingCartItem"/> or <c>null</c> if none was found.</returns>
        public static OrganizedShoppingCartItem FindItemInCart(
            this IList <OrganizedShoppingCartItem> cart,
            ShoppingCartType shoppingCartType,
            Product product,
            ProductVariantAttributeSelection selection,
            Money customerEnteredPrice)
        {
            Guard.NotNull(cart, nameof(cart));
            Guard.NotNull(product, nameof(product));

            // Return on product bundle with individual item pricing - too complex
            if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)
            {
                return(null);
            }

            // Filter non group items from correct cart type, with matching product id and product type id
            var filteredCart = cart
                               .Where(x => x.Item.ShoppingCartType == shoppingCartType &&
                                      x.Item.ParentItemId == null &&
                                      x.Item.Product.ProductTypeId == product.ProductTypeId &&
                                      x.Item.ProductId == product.Id);

            // There could be multiple matching products with the same identifier but different attributes/selections (etc).
            // Ensure matching product infos are the same (attributes, gift card values (if it is gift card), customerEnteredPrice).
            foreach (var cartItem in filteredCart)
            {
                // Compare attribute selection
                var cartItemSelection = cartItem.Item.AttributeSelection;
                if (cartItemSelection != selection)
                {
                    continue;
                }

                var currentProduct = cartItem.Item.Product;

                // Compare gift cards info values (if it is a gift card)
                if (currentProduct.IsGiftCard &&
                    (cartItemSelection.GiftCardInfo == null ||
                     selection.GiftCardInfo == null ||
                     cartItemSelection != selection))
                {
                    continue;
                }

                // Products with CustomerEntersPrice are equal if the price is the same.
                // But a system product may only be placed once in the shopping cart.
                if (currentProduct.CustomerEntersPrice &&
                    !currentProduct.IsSystemProduct &&
                    customerEnteredPrice.RoundedAmount != decimal.Round(cartItem.Item.CustomerEnteredPrice, customerEnteredPrice.DecimalDigits))
                {
                    continue;
                }

                // If we got this far, we found a matching product with the same values
                return(cartItem);
            }

            return(null);
        }
Beispiel #3
0
        internal async Task <decimal> GetCartItemsAttributesWeightAsync(IList <OrganizedShoppingCartItem> cart, bool multipliedByQuantity = true)
        {
            Guard.NotNull(cart, nameof(cart));

            var rawAttributes = cart
                                .Where(x => x.Item.RawAttributes.HasValue())
                                .Select(x => x.Item.RawAttributes);

            var selection = new ProductVariantAttributeSelection(string.Empty);

            foreach (var cartItem in cart)
            {
                if (cartItem.Item.RawAttributes.IsEmpty() || cartItem.Item.Product.IsGiftCard)
                {
                    continue;
                }

                var attributeSelection = new ProductVariantAttributeSelection(cartItem.Item.RawAttributes);
                foreach (var attribute in attributeSelection.AttributesMap)
                {
                    if (attribute.Value.IsNullOrEmpty())
                    {
                        continue;
                    }

                    selection.AddAttribute(attribute.Key, attribute.Value.ToArray());
                }
            }

            var attributeValueIds = selection.GetAttributeValueIds();

            // Gets either all values of attributes without a product linkage
            // or linked products which are shipping enabled
            var query = _db.ProductVariantAttributeValues
                        .Include(x => x.ProductVariantAttribute)
                        .ThenInclude(x => x.Product)
                        .ApplyValueFilter(attributeValueIds)
                        .Where(x => x.ValueTypeId == (int)ProductVariantAttributeValueType.ProductLinkage &&
                               x.ProductVariantAttribute.Product != null &&
                               x.ProductVariantAttribute.Product.IsShippingEnabled ||
                               x.ValueTypeId != (int)ProductVariantAttributeValueType.ProductLinkage);

            // Calculates attributes weight
            // Get attributes without product linkage > add attribute weight adjustment
            var attributesWeight = await query
                                   .Where(x => x.ValueTypeId != (int)ProductVariantAttributeValueType.ProductLinkage)
                                   // TODO: (ms) (core) Test possible SumAsync SQL projection failure (IIF)
                                   .SumAsync(x => x.WeightAdjustment * (multipliedByQuantity ? x.Quantity : 1));

            // TODO: (ms) (core) needs to be tested with NullResult
            // Get attributes with product linkage > add product weigth
            attributesWeight += await query
                                .Where(x => x.ValueTypeId == (int)ProductVariantAttributeValueType.ProductLinkage)
                                .Select(x => new { x.ProductVariantAttribute.Product, x.Quantity })
                                .Where(x => x.Product != null && x.Product.IsShippingEnabled)
                                .SumAsync(x => x.Product.Weight * x.Quantity);

            return(attributesWeight);
        }
        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="selection">The selected product attributes.</param>
        /// <param name="productId">The identifier of the related product.</param>
        public PriceCalculationAttributes(ProductVariantAttributeSelection selection, int productId)
        {
            Guard.NotNull(selection, nameof(selection));
            Guard.NotZero(productId, nameof(productId));

            Selection = selection;
            ProductId = productId;
        }
Beispiel #5
0
        public virtual Task <IList <OrganizedShoppingCartItem> > GetCartItemsAsync(
            Customer customer         = null,
            ShoppingCartType cartType = ShoppingCartType.ShoppingCart,
            int storeId = 0)
        {
            customer ??= _workContext.CurrentCustomer;

            var cacheKey = CartItemsKey.FormatInvariant(customer.Id, (int)cartType, storeId);
            var result   = _requestCache.Get(cacheKey, async() =>
            {
                var cartItems = new List <ShoppingCartItem>();
                // TODO: (ms) (core) Do we need to check for ShoppingCartItems.Product.ProductVariantAttribute is loaded too? Would this direct access be even possible then?
                if (_db.IsCollectionLoaded(customer, x => x.ShoppingCartItems))
                {
                    var filteredCartItems = customer.ShoppingCartItems
                                            .Where(x => x.CustomerId == customer.Id && x.ShoppingCartTypeId == (int)cartType);

                    if (storeId > 0)
                    {
                        filteredCartItems = cartItems.Where(x => x.StoreId == storeId);
                    }

                    cartItems = filteredCartItems.ToList();
                }
                else
                {
                    // TODO: (core) Re-apply data to Customer.ShoppingCartItems collection to prevent reloads.
                    cartItems = await _db.ShoppingCartItems
                                .Include(x => x.Product)
                                .ThenInclude(x => x.ProductVariantAttributes)
                                .ApplyStandardFilter(cartType, storeId, customer)
                                .ToListAsync();
                }

                // Prefetch all product variant attributes
                var allAttributes    = new ProductVariantAttributeSelection(string.Empty);
                var allAttributeMaps = cartItems.SelectMany(x => x.AttributeSelection.AttributesMap);

                foreach (var attribute in allAttributeMaps)
                {
                    if (allAttributes.AttributesMap.Contains(attribute))
                    {
                        continue;
                    }

                    allAttributes.AddAttribute(attribute.Key, attribute.Value);
                }

                // TODO: (ms) (core) Check if this is sufficient and good prefetch -> what about caching or skipping already loaded?
                await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(allAttributes);

                return(await OrganizeCartItemsAsync(cartItems));
            });

            return(result);
        }
Beispiel #6
0
        public virtual Task <List <OrganizedShoppingCartItem> > GetCartItemsAsync(
            Customer customer         = null,
            ShoppingCartType cartType = ShoppingCartType.ShoppingCart,
            int storeId = 0)
        {
            customer ??= _workContext.CurrentCustomer;

            var cacheKey = CartItemsKey.FormatInvariant(customer.Id, (int)cartType, storeId);
            var result   = _requestCache.Get(cacheKey, async() =>
            {
                var cartItems = new List <ShoppingCartItem>();
                if (_db.IsCollectionLoaded(customer, x => x.ShoppingCartItems))
                {
                    var filteredCartItems = customer.ShoppingCartItems
                                            .Where(x => x.CustomerId == customer.Id && x.ShoppingCartTypeId == (int)cartType);

                    if (storeId > 0)
                    {
                        filteredCartItems = cartItems.Where(x => x.StoreId == storeId);
                    }

                    cartItems = filteredCartItems.ToList();
                }
                else
                {
                    cartItems = await _db.ShoppingCartItems
                                .Include(x => x.Product)
                                .ThenInclude(x => x.ProductVariantAttributes)
                                .ApplyStandardFilter(cartType, storeId, customer)
                                .ToListAsync();

                    customer.ShoppingCartItems = cartItems;
                }

                // Prefetch all product variant attributes
                var allAttributes    = new ProductVariantAttributeSelection(string.Empty);
                var allAttributeMaps = cartItems.SelectMany(x => x.AttributeSelection.AttributesMap);

                foreach (var attribute in allAttributeMaps)
                {
                    if (allAttributes.AttributesMap.Contains(attribute))
                    {
                        continue;
                    }

                    allAttributes.AddAttribute(attribute.Key, attribute.Value);
                }

                await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(allAttributes);

                return(await OrganizeCartItemsAsync(cartItems));
            });

            return(result);
        }
Beispiel #7
0
        // TODO: (mg) (core) Describe pricing pipeline when ready.

        public static void AddAttributes(this PriceCalculationContext context, ProductVariantAttributeSelection selection, int productId, int?bundleItemId = null)
        {
            Guard.NotNull(context, nameof(context));

            if (selection?.AttributesMap?.Any() ?? false)
            {
                context.Attributes.Add(new PriceCalculationAttributes(selection, productId)
                {
                    BundleItemId = bundleItemId
                });
            }
        }
Beispiel #8
0
        protected virtual async Task <IList <OrganizedShoppingCartItem> > OrganizeCartItemsAsync(ICollection <ShoppingCartItem> cart)
        {
            var result = new List <OrganizedShoppingCartItem>();

            if (cart.IsNullOrEmpty())
            {
                return(result);
            }

            var parents = cart.Where(x => x.ParentItemId is null);

            // TODO: (ms) (core) to reduce db roundtrips -> load and filter children by parents (id and so on) into lists and try to get from db as batch request

            foreach (var parent in parents)
            {
                var parentItem = new OrganizedShoppingCartItem(parent);

                var children = cart.Where(x => x.ParentItemId != null &&
                                          x.ParentItemId == parent.Id &&
                                          x.Id != parent.Id &&
                                          x.ShoppingCartTypeId == parent.ShoppingCartTypeId &&
                                          x.Product.CanBeBundleItem());

                // TODO: (ms) (core) Reduce database roundtrips in OrganizeCartItemsAsync
                foreach (var child in children)
                {
                    var childItem = new OrganizedShoppingCartItem(child);

                    if (child.RawAttributes.HasValue() &&
                        (parent.Product?.BundlePerItemPricing ?? false) &&
                        child.BundleItem != null)
                    {
                        var selection = new ProductVariantAttributeSelection(child.RawAttributes);

                        await _productAttributeMaterializer.MergeWithCombinationAsync(child.Product, selection);

                        var attributeValues = await _productAttributeMaterializer
                                              .MaterializeProductVariantAttributeValuesAsync(selection);

                        if (!attributeValues.IsNullOrEmpty())
                        {
                            childItem.BundleItemData.AdditionalCharge += attributeValues.Sum(x => x.PriceAdjustment);
                        }
                    }

                    parentItem.ChildItems.Add(childItem);
                }

                result.Add(parentItem);
            }

            return(result);
        }
        /// <summary>
        /// Creates an absolute product URL.
        /// </summary>
        /// <param name="productId">Product identifier.</param>
        /// <param name="productSlug">Product URL slug.</param>
        /// <param name="selection">Selected attributes.</param>
        /// <param name="store">Store.</param>
        /// <param name="language">Language.</param>
        /// <returns>Absolute product URL.</returns>
        public virtual async Task <string> GetAbsoluteProductUrlAsync(
            int productId,
            string productSlug,
            ProductVariantAttributeSelection selection = null,
            Store store       = null,
            Language language = null)
        {
            var request = _httpContextAccessor?.HttpContext?.Request;

            if (request == null || productSlug.IsEmpty())
            {
                return(null);
            }

            var url = Url;

            if (url.IsEmpty())
            {
                store ??= _storeContext.CurrentStore;
                language ??= _workContext.WorkingLanguage;

                string hostName = null;

                try
                {
                    // Do not create crappy URLs (exclude scheme, include port and no slash)!
                    hostName = new Uri(store.GetHost(true)).Authority;
                }
                catch { }

                url = _urlHelper.Value.RouteUrl(
                    "Product",
                    new { SeName = productSlug, culture = language.UniqueSeoCode },
                    store.SupportsHttps() ? "https" : "http",
                    hostName);
            }

            if (selection?.AttributesMap?.Any() ?? false)
            {
                var query = new ProductVariantQuery();
                await AddAttributesToQueryAsync(query, selection, productId);

                url = url.TrimEnd('/') + ToQueryString(query);
            }

            return(url);
        }
        private async Task <MediaFileInfo> GetMediaFileFor(Product product, ProductVariantAttributeSelection attrSelection = null)
        {
            var attrParser   = _services.Resolve <IProductAttributeMaterializer>();
            var mediaService = _services.Resolve <IMediaService>();

            MediaFileInfo file = null;

            if (attrSelection != null)
            {
                var combination = await attrParser.FindAttributeCombinationAsync(product.Id, attrSelection);

                if (combination != null)
                {
                    var fileIds = combination.GetAssignedMediaIds();
                    if (fileIds?.Any() ?? false)
                    {
                        file = await mediaService.GetFileByIdAsync(fileIds[0], MediaLoadFlags.AsNoTracking);
                    }
                }
            }

            if (file == null)
            {
                file = await mediaService.GetFileByIdAsync(product.MainPictureId ?? 0, MediaLoadFlags.AsNoTracking);
            }

            if (file == null && product.Visibility == ProductVisibility.Hidden && product.ParentGroupedProductId > 0)
            {
                var productFile = await _db.ProductMediaFiles
                                  .AsNoTracking()
                                  .Include(x => x.MediaFile)
                                  .ApplyProductFilter(product.ParentGroupedProductId)
                                  .FirstOrDefaultAsync();

                if (productFile?.MediaFile != null)
                {
                    file = mediaService.ConvertMediaFile(productFile.MediaFile);
                }
            }

            return(file);
        }
Beispiel #11
0
        private async Task <ProductAskQuestionModel> PrepareAskQuestionModelAsync(Product product)
        {
            var customer      = Services.WorkContext.CurrentCustomer;
            var rawAttributes = TempData.Peek("AskQuestionAttributeSelection-" + product.Id) as string;

            // Check if saved rawAttributes belongs to current product id
            var formattedAttributes = string.Empty;
            var selection           = new ProductVariantAttributeSelection(rawAttributes);

            if (selection.AttributesMap.Any())
            {
                formattedAttributes = await _productAttributeFormatter.Value.FormatAttributesAsync(
                    selection,
                    product,
                    customer : null,
                    separator : ", ",
                    includePrices : false,
                    includeGiftCardAttributes : false,
                    includeHyperlinks : false);
            }

            var seName = await product.GetActiveSlugAsync();

            var model = new ProductAskQuestionModel
            {
                Id                 = product.Id,
                ProductName        = product.GetLocalized(x => x.Name),
                ProductSeName      = seName,
                SenderEmail        = customer.Email,
                SenderName         = customer.GetFullName(),
                SenderNameRequired = _privacySettings.FullNameOnProductRequestRequired,
                SenderPhone        = customer.GenericAttributes.Phone,
                DisplayCaptcha     = _captchaSettings.CanDisplayCaptcha && _captchaSettings.ShowOnAskQuestionPage,
                SelectedAttributes = formattedAttributes,
                ProductUrl         = await _productUrlHelper.Value.GetProductUrlAsync(product.Id, seName, selection),
                IsQuoteRequest     = product.CallForPrice
            };

            model.Question = T("Products.AskQuestion.Question." + (model.IsQuoteRequest ? "QuoteRequest" : "GeneralInquiry"), model.ProductName);

            return(model);
        }
        public virtual async Task <Money> CalculateProductCostAsync(Product product, ProductVariantAttributeSelection selection = null)
        {
            Guard.NotNull(product, nameof(product));
            Guard.NotNull(selection, nameof(selection));

            var productCost = product.ProductCost;

            if (selection != null)
            {
                var attributeValues = await _productAttributeMaterializer.MaterializeProductVariantAttributeValuesAsync(selection);

                var productLinkageValues = attributeValues
                                           .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage && x.LinkedProductId != 0)
                                           .ToList();
                var linkedProductIds = productLinkageValues
                                       .Select(x => x.LinkedProductId)
                                       .Distinct()
                                       .ToArray();

                if (linkedProductIds.Any())
                {
                    var linkedProducts = await _db.Products
                                         .AsNoTracking()
                                         .Where(x => linkedProductIds.Contains(x.Id))
                                         .Select(x => new { x.Id, x.ProductCost })
                                         .ToListAsync();

                    var linkedProductsDic = linkedProducts.ToDictionarySafe(x => x.Id, x => x.ProductCost);

                    foreach (var value in productLinkageValues)
                    {
                        if (linkedProductsDic.TryGetValue(value.LinkedProductId, out var linkedProductCost))
                        {
                            productCost += linkedProductCost * value.Quantity;
                        }
                    }
                }
            }

            return(new(productCost, _primaryCurrency));
        }
Beispiel #13
0
        /// <summary>
        /// Calculates the unit price for a given shopping cart item.
        /// </summary>
        /// <param name="priceCalculationService">Price calculation service.</param>
        /// <param name="cartItem">Shopping cart item.</param>
        /// <param name="ignoreDiscounts">A value indicating whether to ignore discounts.</param>
        /// <param name="targetCurrency">The target currency to use for money conversion. Obtained from <see cref="IWorkContext.WorkingCurrency"/> if <c>null</c>.</param>
        /// <returns>Calculated unit price.</returns>
        //public static async Task<CalculatedPrice> CalculateUnitPriceAsync(
        //    this IPriceCalculationService2 priceCalculationService,
        //    OrganizedShoppingCartItem cartItem,
        //    bool ignoreDiscounts = false,
        //    Currency targetCurrency = null)
        //{
        //    Guard.NotNull(priceCalculationService, nameof(priceCalculationService));
        //    Guard.NotNull(cartItem, nameof(cartItem));

        //    var options = priceCalculationService.CreateDefaultOptions(false, cartItem.Item.Customer, targetCurrency);
        //    options.IgnoreDiscounts = ignoreDiscounts;

        //    var context = new PriceCalculationContext(cartItem, options);

        //    return await priceCalculationService.CalculatePriceAsync(context);
        //}

        /// <summary>
        /// Calculates both the unit price and the subtotal for a given shopping cart item.
        /// The subtotal is calculated by multiplying the unit price by <see cref="ShoppingCartItem.Quantity"/>.
        /// </summary>
        /// <param name="priceCalculationService">Price calculation service.</param>
        /// <param name="cartItem">Shopping cart item.</param>
        /// <param name="ignoreDiscounts">A value indicating whether to ignore discounts.</param>
        /// <param name="targetCurrency">The target currency to use for money conversion. Obtained from <see cref="IWorkContext.WorkingCurrency"/> if <c>null</c>.</param>
        /// <returns>Calculated subtotal.</returns>
        //public static async Task<(CalculatedPrice UnitPrice, CalculatedPrice Subtotal)> CalculateSubtotalAsync(
        //    this IPriceCalculationService2 priceCalculationService,
        //    OrganizedShoppingCartItem cartItem,
        //    bool ignoreDiscounts = false,
        //    Currency targetCurrency = null)
        //{
        //    Guard.NotNull(priceCalculationService, nameof(priceCalculationService));
        //    Guard.NotNull(cartItem, nameof(cartItem));

        //    var options = priceCalculationService.CreateDefaultOptions(false, cartItem.Item.Customer, targetCurrency);
        //    options.IgnoreDiscounts = ignoreDiscounts;

        //    var context = new PriceCalculationContext(cartItem, options);

        //    return await priceCalculationService.CalculateSubtotalAsync(context);
        //}

        /// <summary>
        /// Calculates the price adjustments of product attributes, usually <see cref="ProductVariantAttributeValue.PriceAdjustment"/>.
        /// Typically used to display price adjustments of selected attributes on the cart page.
        /// The calculated adjustment is always a unit price.
        /// </summary>
        /// <param name="priceCalculationService">Price calculation service.</param>
        /// <param name="product">The product.</param>
        /// <param name="selection">Attribute selection.</param>
        /// <param name="quantity">
        /// The product quantity. May have impact on the price, e.g. if tier prices are applied to price adjustments.
        /// Note that the calculated price is always the unit price.
        /// </param>
        /// <param name="options">Price calculation options. The default options are used if <c>null</c>.</param>
        /// <returns>Price adjustments of selected attributes. Key: <see cref="ProductVariantAttributeValue.Id"/>, value: attribute price adjustment.</returns>
        public static async Task <IDictionary <int, CalculatedPriceAdjustment> > CalculateAttributePriceAdjustmentsAsync(
            this IPriceCalculationService2 priceCalculationService,
            Product product,
            ProductVariantAttributeSelection selection,
            int quantity = 1,
            PriceCalculationOptions options = null)
        {
            Guard.NotNull(priceCalculationService, nameof(priceCalculationService));
            Guard.NotNull(selection, nameof(selection));

            options ??= priceCalculationService.CreateDefaultOptions(false);
            options.DeterminePriceAdjustments = true;

            var pricingContext = new PriceCalculationContext(product, quantity, options);

            pricingContext.AddSelectedAttributes(selection, product.Id);

            var price = await priceCalculationService.CalculatePriceAsync(pricingContext);

            return(price.AttributePriceAdjustments.ToDictionarySafe(x => x.AttributeValue.Id));
        }
Beispiel #14
0
        public virtual Task <List <OrganizedShoppingCartItem> > GetCartItemsAsync(
            Customer customer         = null,
            ShoppingCartType cartType = ShoppingCartType.ShoppingCart,
            int storeId = 0)
        {
            customer ??= _workContext.CurrentCustomer;

            var cacheKey = CartItemsKey.FormatInvariant(customer.Id, (int)cartType, storeId);
            var result   = _requestCache.Get(cacheKey, async() =>
            {
                await _db.LoadCollectionAsync(customer, x => x.ShoppingCartItems, false, x =>
                {
                    return(x
                           .Include(x => x.Product)
                           .ThenInclude(x => x.ProductVariantAttributes));
                });

                var cartItems = customer.ShoppingCartItems.FilterByCartType(cartType, storeId);

                // Prefetch all product variant attributes
                var allAttributes    = new ProductVariantAttributeSelection(string.Empty);
                var allAttributeMaps = cartItems.SelectMany(x => x.AttributeSelection.AttributesMap);

                foreach (var attribute in allAttributeMaps)
                {
                    if (allAttributes.AttributesMap.Contains(attribute))
                    {
                        continue;
                    }

                    allAttributes.AddAttribute(attribute.Key, attribute.Value);
                }

                await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(allAttributes);

                return(await OrganizeCartItemsAsync(cartItems));
            });

            return(result);
        }
Beispiel #15
0
        private async Task ProcessAttributes(DbContextScope scope, Product product, Product clone, IEnumerable <Language> languages)
        {
            var localizedKeySelectors = new List <Expression <Func <ProductVariantAttributeValue, string> > >
            {
                x => x.Name,
                x => x.Alias
            };

            await _db.LoadCollectionAsync(product, x => x.ProductVariantAttributes);

            await _db.LoadCollectionAsync(product, x => x.ProductVariantAttributeCombinations);

            // Former attribute id > clone.
            var attributeMap = new Dictionary <int, ProductVariantAttribute>();
            // Former attribute value id > clone.
            var valueMap        = new Dictionary <int, ProductVariantAttributeValue>();
            var newCombinations = new List <ProductVariantAttributeCombination>();

            // Product attributes.
            foreach (var attribute in product.ProductVariantAttributes)
            {
                // Save associated value (used for combinations copying).
                attributeMap[attribute.Id] = new ProductVariantAttribute
                {
                    ProductId              = clone.Id,
                    ProductAttributeId     = attribute.ProductAttributeId,
                    TextPrompt             = attribute.TextPrompt,
                    IsRequired             = attribute.IsRequired,
                    AttributeControlTypeId = attribute.AttributeControlTypeId,
                    DisplayOrder           = attribute.DisplayOrder
                };
            }

            // Reverse tracking order to have the clones in the same order in the database as the originals.
            _db.ProductVariantAttributes.AddRange(attributeMap.Select(x => x.Value).Reverse());

            // >>>>>> Commit attributes.
            await scope.CommitAsync();

            // Product variant attribute values.
            foreach (var attribute in product.ProductVariantAttributes)
            {
                var attributeClone = attributeMap[attribute.Id];

                foreach (var value in attribute.ProductVariantAttributeValues)
                {
                    // Save associated value (used for combinations copying).
                    valueMap.Add(value.Id, new ProductVariantAttributeValue
                    {
                        ProductVariantAttributeId = attributeClone.Id,
                        Name             = value.Name,
                        Color            = value.Color,
                        PriceAdjustment  = value.PriceAdjustment,
                        WeightAdjustment = value.WeightAdjustment,
                        IsPreSelected    = value.IsPreSelected,
                        DisplayOrder     = value.DisplayOrder,
                        ValueTypeId      = value.ValueTypeId,
                        LinkedProductId  = value.LinkedProductId,
                        Quantity         = value.Quantity,
                        MediaFileId      = value.MediaFileId
                    });
                }
            }

            // Reverse tracking order to have the clones in the same order in the database as the originals.
            _db.ProductVariantAttributeValues.AddRange(valueMap.Select(x => x.Value).Reverse());

            // >>>>>> Commit attribute values.
            await scope.CommitAsync();

            // Attribute value localization.
            var allValues = product.ProductVariantAttributes
                            .Reverse()
                            .SelectMany(x => x.ProductVariantAttributeValues.Reverse())
                            .ToArray();

            foreach (var value in allValues)
            {
                if (valueMap.TryGetValue(value.Id, out var newValue))
                {
                    await ProcessLocalizations(value, newValue, localizedKeySelectors, languages);
                }
            }

            // >>>>>> Commit localized values.
            await scope.CommitAsync();

            // Attribute combinations.
            foreach (var combination in product.ProductVariantAttributeCombinations)
            {
                var oldAttributesMap = combination.AttributeSelection.AttributesMap;
                var oldAttributes    = await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(combination.AttributeSelection);

                var newSelection = new ProductVariantAttributeSelection(null);

                foreach (var oldAttribute in oldAttributes)
                {
                    if (attributeMap.TryGetValue(oldAttribute.Id, out var newAttribute))
                    {
                        var item = oldAttributesMap.FirstOrDefault(x => x.Key == oldAttribute.Id);
                        if (item.Key != 0)
                        {
                            foreach (var value in item.Value)
                            {
                                if (newAttribute.IsListTypeAttribute())
                                {
                                    var oldValueId = value.ToString().EmptyNull().ToInt();
                                    if (valueMap.TryGetValue(oldValueId, out var newValue))
                                    {
                                        newSelection.AddAttributeValue(newAttribute.Id, newValue.Id);
                                    }
                                }
                                else
                                {
                                    newSelection.AddAttributeValue(newAttribute.Id, value);
                                }
                            }
                        }
                    }
                }

                newCombinations.Add(new ProductVariantAttributeCombination
                {
                    ProductId             = clone.Id,
                    RawAttributes         = newSelection.AsJson(),
                    StockQuantity         = combination.StockQuantity,
                    AllowOutOfStockOrders = combination.AllowOutOfStockOrders,
                    Sku  = combination.Sku,
                    Gtin = combination.Gtin,
                    ManufacturerPartNumber = combination.ManufacturerPartNumber,
                    Price = combination.Price,
                    AssignedMediaFileIds = combination.AssignedMediaFileIds,
                    Length              = combination.Length,
                    Width               = combination.Width,
                    Height              = combination.Height,
                    BasePriceAmount     = combination.BasePriceAmount,
                    BasePriceBaseAmount = combination.BasePriceBaseAmount,
                    DeliveryTimeId      = combination.DeliveryTimeId,
                    QuantityUnitId      = combination.QuantityUnitId,
                    IsActive            = combination.IsActive
                                          //IsDefaultCombination = combination.IsDefaultCombination
                });
            }

            // Reverse tracking order to have the clones in the same order in the database as the originals.
            _db.ProductVariantAttributeCombinations.AddRange(newCombinations.AsEnumerable().Reverse());

            // >>>>>> Commit combinations.
            await scope.CommitAsync();
        }
Beispiel #16
0
        protected async Task <ImageModel> PrepareCartItemPictureModelAsync(Product product, int pictureSize, string productName, ProductVariantAttributeSelection attributeSelection)
        {
            Guard.NotNull(product, nameof(product));
            Guard.NotNull(attributeSelection, nameof(attributeSelection));

            MediaFileInfo file        = null;
            var           combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, attributeSelection);

            if (combination != null)
            {
                var fileIds = combination.GetAssignedMediaIds();
                if (fileIds?.Any() ?? false)
                {
                    file = await _mediaService.GetFileByIdAsync(fileIds[0], MediaLoadFlags.AsNoTracking);
                }
            }

            // No attribute combination image, then load product picture.
            if (file == null)
            {
                var productMediaFile = await _db.ProductMediaFiles
                                       .Include(x => x.MediaFile)
                                       .Where(x => x.Id == product.Id)
                                       .OrderBy(x => x.DisplayOrder)
                                       .FirstOrDefaultAsync();

                if (productMediaFile != null)
                {
                    file = _mediaService.ConvertMediaFile(productMediaFile.MediaFile);
                }
            }

            // Let's check whether this product has some parent "grouped" product.
            if (file == null && product.Visibility == ProductVisibility.Hidden && product.ParentGroupedProductId > 0)
            {
                var productMediaFile = await _db.ProductMediaFiles
                                       .Include(x => x.MediaFile)
                                       .Where(x => x.Id == product.ParentGroupedProductId)
                                       .OrderBy(x => x.DisplayOrder)
                                       .FirstOrDefaultAsync();

                if (productMediaFile != null)
                {
                    file = _mediaService.ConvertMediaFile(productMediaFile.MediaFile);
                }
            }

            var pm = new ImageModel
            {
                Id        = file?.Id ?? 0,
                ThumbSize = pictureSize,
                Host      = _mediaService.GetUrl(file, pictureSize, null, !_catalogSettings.HideProductDefaultPictures),
                Title     = file?.File?.GetLocalized(x => x.Title)?.Value.NullEmpty() ?? T("Media.Product.ImageLinkTitleFormat", productName),
                Alt       = file?.File?.GetLocalized(x => x.Alt)?.Value.NullEmpty() ?? T("Media.Product.ImageAlternateTextFormat", productName),
                File      = file
            };

            return(pm);
        }
        /// <summary>
        /// Creates a product URL including variant query string.
        /// </summary>
        /// <param name="productId">Product identifier.</param>
        /// <param name="productSlug">Product URL slug.</param>
        /// <param name="selection">Selected attributes.</param>
        /// <returns>Product URL.</returns>
        public virtual async Task <string> GetProductUrlAsync(int productId, string productSlug, ProductVariantAttributeSelection selection)
        {
            var query = new ProductVariantQuery();

            await AddAttributesToQueryAsync(query, selection, productId);

            return(GetProductUrl(productSlug, query));
        }
        /// <summary>
        /// Adds selected product variant attributes to a product variant query.
        /// </summary>
        /// <param name="query">Target product variant query.</param>
        /// <param name="source">Selected attributes.</param>
        /// <param name="productId">Product identifier.</param>
        /// <param name="bundleItemId">Bundle item identifier.</param>
        /// <param name="attributes">Product variant attributes.</param>
        public virtual async Task AddAttributesToQueryAsync(
            ProductVariantQuery query,
            ProductVariantAttributeSelection source,
            int productId,
            int bundleItemId = 0,
            ICollection <ProductVariantAttribute> attributes = null)
        {
            Guard.NotNull(query, nameof(query));

            if (productId == 0 || !(source?.AttributesMap?.Any() ?? false))
            {
                return;
            }

            if (attributes == null)
            {
                var ids = source.AttributesMap.Select(x => x.Key);
                attributes = await _db.ProductVariantAttributes.GetManyAsync(ids);
            }

            var languageId = _workContext.WorkingLanguage.Id;

            foreach (var attribute in attributes)
            {
                var item = source.AttributesMap.FirstOrDefault(x => x.Key == attribute.Id);
                if (item.Key != 0)
                {
                    foreach (var originalValue in item.Value)
                    {
                        var      value = originalValue.ToString();
                        DateTime?date  = null;

                        if (attribute.AttributeControlType == AttributeControlType.Datepicker)
                        {
                            date = value.ToDateTime(new[] { "D" }, CultureInfo.CurrentCulture, DateTimeStyles.None, null);
                            if (date == null)
                            {
                                continue;
                            }

                            value = string.Join("-", date.Value.Year, date.Value.Month, date.Value.Day);
                        }

                        var queryItem = new ProductVariantQueryItem(value)
                        {
                            ProductId          = productId,
                            BundleItemId       = bundleItemId,
                            AttributeId        = attribute.ProductAttributeId,
                            VariantAttributeId = attribute.Id,
                            Alias  = _catalogSearchQueryAliasMapper.Value.GetVariantAliasById(attribute.ProductAttributeId, languageId),
                            Date   = date,
                            IsFile = attribute.AttributeControlType == AttributeControlType.FileUpload,
                            IsText = attribute.AttributeControlType == AttributeControlType.TextBox || attribute.AttributeControlType == AttributeControlType.MultilineTextbox
                        };

                        if (attribute.IsListTypeAttribute())
                        {
                            queryItem.ValueAlias = _catalogSearchQueryAliasMapper.Value.GetVariantOptionAliasById(value.ToInt(), languageId);
                        }

                        query.AddVariant(queryItem);
                    }
                }
            }
        }
        public virtual async Task <AdjustInventoryResult> AdjustInventoryAsync(Product product, ProductVariantAttributeSelection selection, bool decrease, int quantity)
        {
            Guard.NotNull(product, nameof(product));
            Guard.NotNull(selection, nameof(selection));

            var result = new AdjustInventoryResult();

            switch (product.ManageInventoryMethod)
            {
            case ManageInventoryMethod.ManageStock:
            {
                result.StockQuantityOld = product.StockQuantity;

                result.StockQuantityNew = decrease
                            ? product.StockQuantity - quantity
                            : product.StockQuantity + quantity;

                var newPublished             = product.Published;
                var newDisableBuyButton      = product.DisableBuyButton;
                var newDisableWishlistButton = product.DisableWishlistButton;

                // Check if the minimum quantity is reached.
                switch (product.LowStockActivity)
                {
                case LowStockActivity.DisableBuyButton:
                    newDisableBuyButton      = product.MinStockQuantity >= result.StockQuantityNew;
                    newDisableWishlistButton = product.MinStockQuantity >= result.StockQuantityNew;
                    break;

                case LowStockActivity.Unpublish:
                    newPublished = product.MinStockQuantity <= result.StockQuantityNew;
                    break;
                }

                product.StockQuantity         = result.StockQuantityNew;
                product.DisableBuyButton      = newDisableBuyButton;
                product.DisableWishlistButton = newDisableWishlistButton;
                product.Published             = newPublished;

                // Commit required because of store owner notification.
                await _db.SaveChangesAsync();

                if (decrease && product.NotifyAdminForQuantityBelow > result.StockQuantityNew)
                {
                    await _messageFactory.SendQuantityBelowStoreOwnerNotificationAsync(product, _localizationSettings.DefaultAdminLanguageId);
                }
            }
            break;

            case ManageInventoryMethod.ManageStockByAttributes:
            {
                var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, selection);

                if (combination != null)
                {
                    result.StockQuantityOld = combination.StockQuantity;

                    result.StockQuantityNew = decrease
                                ? combination.StockQuantity - quantity
                                : combination.StockQuantity + quantity;

                    combination.StockQuantity = result.StockQuantityNew;
                }
            }
            break;

            case ManageInventoryMethod.DontManageStock:
            default:
                // Do nothing.
                break;
            }

            var attributeValues = await _productAttributeMaterializer.MaterializeProductVariantAttributeValuesAsync(selection);

            var productLinkageValues = attributeValues
                                       .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage)
                                       .ToList();

            foreach (var chunk in productLinkageValues.Slice(100))
            {
                var linkedProductIds = chunk.Select(x => x.LinkedProductId).Distinct().ToArray();
                var linkedProducts   = await _db.Products.GetManyAsync(linkedProductIds, true);

                var linkedProductsDic = linkedProducts.ToDictionarySafe(x => x.Id);

                foreach (var value in chunk)
                {
                    if (linkedProductsDic.TryGetValue(value.LinkedProductId, out var linkedProduct))
                    {
                        await AdjustInventoryAsync(linkedProduct, null, decrease, quantity *value.Quantity);
                    }
                }
            }

            await _db.SaveChangesAsync();

            return(result);
        }
Beispiel #20
0
        public virtual async Task <AdjustInventoryResult> AdjustInventoryAsync(Product product, ProductVariantAttributeSelection selection, bool decrease, int quantity)
        {
            Guard.NotNull(product, nameof(product));
            Guard.NotNull(selection, nameof(selection));

            var result = new AdjustInventoryResult();

            switch (product.ManageInventoryMethod)
            {
            case ManageInventoryMethod.ManageStock:
            {
                result.StockQuantityOld = product.StockQuantity;

                result.StockQuantityNew = decrease
                            ? product.StockQuantity - quantity
                            : product.StockQuantity + quantity;

                var newPublished             = product.Published;
                var newDisableBuyButton      = product.DisableBuyButton;
                var newDisableWishlistButton = product.DisableWishlistButton;

                // Check if the minimum quantity is reached.
                switch (product.LowStockActivity)
                {
                case LowStockActivity.DisableBuyButton:
                    newDisableBuyButton      = product.MinStockQuantity >= result.StockQuantityNew;
                    newDisableWishlistButton = product.MinStockQuantity >= result.StockQuantityNew;
                    break;

                case LowStockActivity.Unpublish:
                    newPublished = product.MinStockQuantity <= result.StockQuantityNew;
                    break;
                }

                product.StockQuantity         = result.StockQuantityNew;
                product.DisableBuyButton      = newDisableBuyButton;
                product.DisableWishlistButton = newDisableWishlistButton;
                product.Published             = newPublished;

                // TODO: (mg) (core) ProductService.AdjustInventoryAsync doesn't send SendQuantityBelowStoreOwnerNotification anymore. Must be sent by caller after (!) database commit.
                // TODO: (mg) (core) The caller should definitely NOT be responsible for figuring out, when and how to publish messages. That would be extremely bad API design.
                //if (decrease && product.NotifyAdminForQuantityBelow > result.StockQuantityNew)
                //{
                //    _services.MessageFactory.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId);
                //}
            }
            break;

            case ManageInventoryMethod.ManageStockByAttributes:
            {
                var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, selection);

                if (combination != null)
                {
                    result.StockQuantityOld = combination.StockQuantity;

                    result.StockQuantityNew = decrease
                                ? combination.StockQuantity - quantity
                                : combination.StockQuantity + quantity;

                    combination.StockQuantity = result.StockQuantityNew;
                }
            }
            break;

            case ManageInventoryMethod.DontManageStock:
            default:
                // Do nothing.
                break;
            }

            var attributeValues = await _productAttributeMaterializer.MaterializeProductVariantAttributeValuesAsync(selection);

            var productLinkageValues = attributeValues
                                       .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage)
                                       .ToList();

            foreach (var chunk in productLinkageValues.Slice(100))
            {
                var linkedProductIds = chunk.Select(x => x.LinkedProductId).Distinct().ToArray();
                var linkedProducts   = await _db.Products.GetManyAsync(linkedProductIds, true);

                var linkedProductsDic = linkedProducts.ToDictionarySafe(x => x.Id);

                foreach (var value in chunk)
                {
                    if (linkedProductsDic.TryGetValue(value.LinkedProductId, out var linkedProduct))
                    {
                        await AdjustInventoryAsync(linkedProduct, null, decrease, quantity *value.Quantity);
                    }
                }
            }

            return(result);
        }