Beispiel #1
0
        /// <summary>
        /// Gets a list of product variant attribute values from an attribute selection.
        /// Loads attribute values from <paramref name="attributes"/> and not from database.
        /// Typically used in conjunction with <see cref="ProductBatchContext"/>.
        /// Only returns values of list type attributes (<see cref="ProductVariantAttribute.IsListTypeAttribute()"/>).
        /// </summary>
        /// <param name="selection">Attributes selection.</param>
        /// <param name="attributes">Attributes from which the values are loaded.</param>
        /// <returns>List of product variant attribute values.</returns>
        public static IList <ProductVariantAttributeValue> MaterializeProductVariantAttributeValues(
            this ProductVariantAttributeSelection selection,
            IEnumerable <ProductVariantAttribute> attributes)
        {
            Guard.NotNull(attributes, nameof(attributes));

            var result = new List <ProductVariantAttributeValue>();

            if (selection?.AttributesMap?.Any() ?? false)
            {
                var listTypeAttributeIds = attributes
                                           .Where(x => x.IsListTypeAttribute())
                                           .OrderBy(x => x.DisplayOrder)
                                           .Select(x => x.Id)
                                           .Distinct()
                                           .ToArray();

                var valueIds = selection.AttributesMap
                               .Where(x => listTypeAttributeIds.Contains(x.Key))
                               .SelectMany(x => x.Value)
                               .Select(x => x.ToString())
                               .Where(x => x.HasValue()) // Avoid exception when string is empty.
                               .Select(x => x.ToInt())
                               .Where(x => x != 0)
                               .Distinct()
                               .ToArray();

                foreach (int valueId in valueIds)
                {
                    foreach (var attribute in attributes)
                    {
                        var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == valueId);
                        if (attributeValue != null)
                        {
                            result.Add(attributeValue);
                            break;
                        }
                    }
                }
            }

            return(result);
        }
        // TODO: (mg) (core) Check whether IProductAttributeMaterializer.PrefetchProductVariantAttributes is still required.
        // Looks like it can be done by MaterializeProductVariantAttributeValuesAsync.

        public virtual async Task <IList <ProductVariantAttribute> > MaterializeProductVariantAttributesAsync(ProductVariantAttributeSelection selection)
        {
            Guard.NotNull(selection, nameof(selection));

            var ids = selection.AttributesMap
                      .Select(x => x.Key)
                      .ToArray();

            if (!ids.Any())
            {
                return(new List <ProductVariantAttribute>());
            }

            var cacheKey = ATTRIBUTES_BY_IDS_KEY + string.Join(",", ids);

            var result = await _requestCache.GetAsync(cacheKey, async() =>
            {
                var query = _db.ProductVariantAttributes
                            .AsNoTracking()
                            .Include(x => x.Product)
                            .Include(x => x.ProductAttribute)
                            .Include(x => x.ProductVariantAttributeValues)
                            .Where(x => ids.Contains(x.Id))
                            .OrderBy(x => x.DisplayOrder);

                var attributes = await query.ToListAsync();

                return(attributes.OrderBySequence(ids).ToList());
            });

            return(result);
        }
        public virtual async Task <ProductVariantAttributeCombination> MergeWithCombinationAsync(Product product, ProductVariantAttributeSelection selection)
        {
            var combination = await FindAttributeCombinationAsync(product.Id, selection);

            if (combination != null && combination.IsActive)
            {
                product.MergeWithCombination(combination);
            }
            else if (product.MergedDataValues != null)
            {
                product.MergedDataValues.Clear();
            }

            return(combination);
        }
        public virtual async Task <ProductVariantAttributeCombination> FindAttributeCombinationAsync(int productId, ProductVariantAttributeSelection selection)
        {
            if (productId == 0 || !(selection?.AttributesMap?.Any() ?? false))
            {
                return(null);
            }

            var cacheKey = ATTRIBUTECOMBINATION_BY_IDJSON_KEY.FormatInvariant(productId, selection.AsJson());

            var combinations = await _requestCache.GetAsync(cacheKey, async() =>
            {
                var combinations = await _db.ProductVariantAttributeCombinations
                                   .AsNoTracking()
                                   .Where(x => x.ProductId == productId)
                                   .Select(x => new { x.Id, x.RawAttributes })
                                   .ToListAsync();

                foreach (var combination in combinations)
                {
                    if (selection.Equals(new ProductVariantAttributeSelection(combination.RawAttributes)))
                    {
                        return(await _db.ProductVariantAttributeCombinations.FindByIdAsync(combination.Id));
                    }
                }

                return(null);
            });

            return(null);
        }
        public virtual async Task <(ProductVariantAttributeSelection Selection, List <string> Warnings)> CreateAttributeSelectionAsync(
            ProductVariantQuery query,
            IEnumerable <ProductVariantAttribute> attributes,
            int productId,
            int bundleItemId,
            bool getFilesFromRequest = true)
        {
            Guard.NotNull(query, nameof(query));
            Guard.NotNull(attributes, nameof(attributes));

            var selection = new ProductVariantAttributeSelection(null);
            var warnings  = new List <string>();

            foreach (var pva in attributes)
            {
                var selectedItems = query.Variants.Where(x =>
                                                         x.ProductId == productId &&
                                                         x.BundleItemId == bundleItemId &&
                                                         x.AttributeId == pva.ProductAttributeId &&
                                                         x.VariantAttributeId == pva.Id);

                switch (pva.AttributeControlType)
                {
                case AttributeControlType.DropdownList:
                case AttributeControlType.RadioList:
                case AttributeControlType.Boxes:
                {
                    var valueId = selectedItems.FirstOrDefault()
                                  ?.Value
                                  ?.SplitSafe(",")
                                  ?.FirstOrDefault()
                                  ?.ToInt() ?? 0;

                    if (valueId > 0)
                    {
                        selection.AddAttributeValue(pva.Id, valueId);
                    }
                }
                break;

                case AttributeControlType.Checkboxes:
                    foreach (var item in selectedItems)
                    {
                        var valueId = item.Value.SplitSafe(",").FirstOrDefault()?.ToInt() ?? 0;
                        if (valueId > 0)
                        {
                            selection.AddAttributeValue(pva.Id, valueId);
                        }
                    }
                    break;

                case AttributeControlType.TextBox:
                case AttributeControlType.MultilineTextbox:
                {
                    var value = string.Join(",", selectedItems.Select(x => x.Value));
                    if (value.HasValue())
                    {
                        selection.AddAttributeValue(pva.Id, value);
                    }
                }
                break;

                case AttributeControlType.Datepicker:
                    var firstItemDate = selectedItems.FirstOrDefault()?.Date;
                    if (firstItemDate.HasValue)
                    {
                        selection.AddAttributeValue(pva.Id, firstItemDate.Value.ToString("D"));
                    }
                    break;

                case AttributeControlType.FileUpload:
                    if (getFilesFromRequest)
                    {
                        var files = _httpContextAccessor?.HttpContext?.Request?.Form?.Files;
                        if (files?.Any() ?? false)
                        {
                            var postedFile = files[ProductVariantQueryItem.CreateKey(productId, bundleItemId, pva.ProductAttributeId, pva.Id)];
                            if (postedFile != null && postedFile.FileName.HasValue())
                            {
                                if (postedFile.Length > _catalogSettings.Value.FileUploadMaximumSizeBytes)
                                {
                                    warnings.Add(T("ShoppingCart.MaximumUploadedFileSize", (int)(_catalogSettings.Value.FileUploadMaximumSizeBytes / 1024)));
                                }
                                else
                                {
                                    var download = new Download
                                    {
                                        DownloadGuid   = Guid.NewGuid(),
                                        UseDownloadUrl = false,
                                        DownloadUrl    = string.Empty,
                                        UpdatedOnUtc   = DateTime.UtcNow,
                                        EntityId       = productId,
                                        EntityName     = "ProductAttribute"
                                    };

                                    using var stream = postedFile.OpenReadStream();
                                    await _downloadService.Value.InsertDownloadAsync(download, stream, postedFile.FileName);

                                    selection.AddAttributeValue(pva.Id, download.DownloadGuid.ToString());
                                }
                            }
                        }
                    }
                    else if (Guid.TryParse(selectedItems.FirstOrDefault()?.Value, out var downloadGuid) && downloadGuid != Guid.Empty)
                    {
                        var download = await _db.Downloads.Where(x => x.DownloadGuid == downloadGuid).FirstOrDefaultAsync();

                        if (download != null)
                        {
                            if (download.IsTransient)
                            {
                                download.IsTransient = false;
                                await _db.SaveChangesAsync();
                            }

                            selection.AddAttributeValue(pva.Id, download.DownloadGuid.ToString());
                        }
                    }
                    break;
                }
            }

            return(selection, warnings);
        }
        // TODO: (mg) (core) Check caller's return value handling of MaterializeProductVariantAttributeValuesAsync (now returns IList instead of ICollection).
        public virtual async Task <IList <ProductVariantAttributeValue> > MaterializeProductVariantAttributeValuesAsync(ProductVariantAttributeSelection selection)
        {
            Guard.NotNull(selection, nameof(selection));

            var ids = selection.GetAttributeValueIds();

            if (!ids.Any())
            {
                return(new List <ProductVariantAttributeValue>());
            }

            var cacheKey = ATTRIBUTEVALUES_BY_IDS_KEY + string.Join(",", ids);

            var result = await _requestCache.GetAsync(cacheKey, async() =>
            {
                // Only consider values of list control types. Otherwise for instance text entered in a text-box is misinterpreted as an attribute value id.
                var query = _db.ProductVariantAttributeValues
                            .Include(x => x.ProductVariantAttribute)
                            .ThenInclude(x => x.ProductAttribute)
                            .AsNoTracking()
                            .ApplyValueFilter(ids);

                return(await query.ToListAsync());
            });

            // That's what the old ported code did:
            //if (selection?.AttributesMap?.Any() ?? false)
            //{
            //    var pvaIds = selection.AttributesMap.Select(x => x.Key).ToArray();
            //    if (pvaIds.Any())
            //    {
            //        var attributes = await _db.ProductVariantAttributes
            //            .AsNoTracking()
            //            .AsCaching(ProductAttributesCacheDuration)
            //            .Where(x => pvaIds.Contains(x.Id))
            //            .OrderBy(x => x.DisplayOrder)
            //            .ToListAsync();

            //        var valueIds = GetAttributeValueIds(attributes, selection).ToArray();

            //        var values = await _db.ProductVariantAttributeValues
            //            .AsNoTracking()
            //            .AsCaching(ProductAttributesCacheDuration)
            //            .ApplyValueFilter(valueIds)
            //            .ToListAsync();

            //        return values;
            //    }
            //}

            return(result);
        }
Beispiel #7
0
        public virtual async Task <string> FormatAttributesAsync(
            ProductVariantAttributeSelection selection,
            Product product,
            Customer customer                = null,
            string separator                 = "<br />",
            bool htmlEncode                  = true,
            bool includePrices               = true,
            bool includeProductAttributes    = true,
            bool includeGiftCardAttributes   = true,
            bool includeHyperlinks           = true,
            ProductBatchContext batchContext = null)
        {
            Guard.NotNull(selection, nameof(selection));
            Guard.NotNull(product, nameof(product));

            customer ??= _workContext.CurrentCustomer;

            using var pool = StringBuilderPool.Instance.Get(out var result);

            if (includeProductAttributes)
            {
                var languageId = _workContext.WorkingLanguage.Id;
                var attributes = await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(selection);

                var attributesDic = attributes.ToDictionary(x => x.Id);

                // Key: ProductVariantAttributeValue.Id, value: calculated attribute price adjustment.
                var priceAdjustments = includePrices && _catalogSettings.ShowVariantCombinationPriceAdjustment
                    ? await _priceCalculationService.CalculateAttributePriceAdjustmentsAsync(product, selection, 1, _priceCalculationService.CreateDefaultOptions(false, customer, null, batchContext))
                    : new Dictionary <int, CalculatedPriceAdjustment>();

                foreach (var kvp in selection.AttributesMap)
                {
                    if (!attributesDic.TryGetValue(kvp.Key, out var pva))
                    {
                        continue;
                    }

                    foreach (var value in kvp.Value)
                    {
                        var valueStr     = value.ToString().EmptyNull();
                        var pvaAttribute = string.Empty;

                        if (pva.IsListTypeAttribute())
                        {
                            var pvaValue = pva.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == valueStr.ToInt());
                            if (pvaValue != null)
                            {
                                pvaAttribute = "{0}: {1}".FormatInvariant(
                                    pva.ProductAttribute.GetLocalized(x => x.Name, languageId),
                                    pvaValue.GetLocalized(x => x.Name, languageId));

                                if (includePrices)
                                {
                                    if (_shoppingCartSettings.ShowLinkedAttributeValueQuantity &&
                                        pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage &&
                                        pvaValue.Quantity > 1)
                                    {
                                        pvaAttribute = pvaAttribute + " × " + pvaValue.Quantity;
                                    }

                                    if (priceAdjustments.TryGetValue(pvaValue.Id, out var adjustment))
                                    {
                                        if (adjustment.Price > 0)
                                        {
                                            pvaAttribute += $" (+{adjustment.Price})";
                                        }
                                        else if (adjustment.Price < 0)
                                        {
                                            pvaAttribute += $" (-{adjustment.Price * -1})";
                                        }
                                    }
                                }

                                if (htmlEncode)
                                {
                                    pvaAttribute = pvaAttribute.HtmlEncode();
                                }
                            }
                        }
                        else if (pva.AttributeControlType == AttributeControlType.MultilineTextbox)
                        {
                            string attributeName = pva.ProductAttribute.GetLocalized(x => x.Name, languageId);

                            pvaAttribute = "{0}: {1}".FormatInvariant(
                                htmlEncode ? attributeName.HtmlEncode() : attributeName,
                                HtmlUtils.ConvertPlainTextToHtml(valueStr.HtmlEncode()));
                        }
                        else if (pva.AttributeControlType == AttributeControlType.FileUpload)
                        {
                            if (Guid.TryParse(valueStr, out var downloadGuid) && downloadGuid != Guid.Empty)
                            {
                                var download = await _db.Downloads
                                               .AsNoTracking()
                                               .Include(x => x.MediaFile)
                                               .Where(x => x.DownloadGuid == downloadGuid)
                                               .FirstOrDefaultAsync();

                                if (download?.MediaFile != null)
                                {
                                    var attributeText = string.Empty;
                                    var fileName      = htmlEncode
                                        ? download.MediaFile.Name.HtmlEncode()
                                        : download.MediaFile.Name;

                                    if (includeHyperlinks)
                                    {
                                        // TODO: (core) add a method for getting URL (use routing because it handles all SEO friendly URLs).
                                        var downloadLink = _webHelper.GetStoreLocation(false) + "download/getfileupload/?downloadId=" + download.DownloadGuid;
                                        attributeText = $"<a href=\"{downloadLink}\" class=\"fileuploadattribute\">{fileName}</a>";
                                    }
                                    else
                                    {
                                        attributeText = fileName;
                                    }

                                    string attributeName = pva.ProductAttribute.GetLocalized(a => a.Name, languageId);

                                    pvaAttribute = "{0}: {1}".FormatInvariant(
                                        htmlEncode ? attributeName.HtmlEncode() : attributeName,
                                        attributeText);
                                }
                            }
                        }
                        else
                        {
                            // TextBox, Datepicker
                            pvaAttribute = "{0}: {1}".FormatInvariant(pva.ProductAttribute.GetLocalized(x => x.Name, languageId), valueStr);

                            if (htmlEncode)
                            {
                                pvaAttribute = pvaAttribute.HtmlEncode();
                            }
                        }

                        result.Grow(pvaAttribute, separator);
                    }
                }
            }

            if (includeGiftCardAttributes && product.IsGiftCard)
            {
                var gci = selection.GiftCardInfo;
                if (gci != null)
                {
                    // Sender.
                    var giftCardFrom = product.GiftCardType == GiftCardType.Virtual
                        ? (await _localizationService.GetResourceAsync("GiftCardAttribute.From.Virtual")).FormatInvariant(gci.SenderName, gci.SenderEmail)
                        : (await _localizationService.GetResourceAsync("GiftCardAttribute.From.Physical")).FormatInvariant(gci.SenderName);

                    // Recipient.
                    var giftCardFor = product.GiftCardType == GiftCardType.Virtual
                        ? (await _localizationService.GetResourceAsync("GiftCardAttribute.For.Virtual")).FormatInvariant(gci.RecipientName, gci.RecipientEmail)
                        : (await _localizationService.GetResourceAsync("GiftCardAttribute.For.Physical")).FormatInvariant(gci.RecipientName);

                    if (htmlEncode)
                    {
                        giftCardFrom = giftCardFrom.HtmlEncode();
                        giftCardFor  = giftCardFor.HtmlEncode();
                    }

                    result.Grow(giftCardFrom, separator);
                    result.Grow(giftCardFor, separator);
                }
            }

            return(result.ToString());
        }
Beispiel #8
0
        // TODO: (mg) (core) Check whether IProductAttributeMaterializer.PrefetchProductVariantAttributes is still required.
        // Looks like it can be done by MaterializeProductVariantAttributeValuesAsync.

        public virtual async Task <IList <ProductVariantAttribute> > MaterializeProductVariantAttributesAsync(ProductVariantAttributeSelection selection)
        {
            Guard.NotNull(selection, nameof(selection));

            var pvaIds = selection.AttributesMap
                         .Select(x => x.Key)
                         .ToArray();

            if (pvaIds.Any())
            {
                var query = _db.ProductVariantAttributes
                            .Include(x => x.ProductAttribute)
                            .Include(x => x.ProductVariantAttributeValues)
                            .AsNoTracking()
                            .AsCaching(ProductAttributesCacheDuration)
                            .Where(x => pvaIds.Contains(x.Id));

                var attributes = await query.ToListAsync();

                return(attributes.OrderBySequence(pvaIds).ToList());
            }

            return(new List <ProductVariantAttribute>());
        }
Beispiel #9
0
        public virtual async Task <CombinationAvailabilityInfo> IsCombinationAvailableAsync(
            Product product,
            IEnumerable <ProductVariantAttribute> attributes,
            IEnumerable <ProductVariantAttributeValue> selectedValues,
            ProductVariantAttributeValue currentValue)
        {
            if (product == null ||
                _performanceSettings.MaxUnavailableAttributeCombinations <= 0 ||
                !(selectedValues?.Any() ?? false))
            {
                return(null);
            }

            // Get unavailable combinations.
            var unavailableCombinations = await _cache.GetAsync(UNAVAILABLE_COMBINATIONS_KEY.FormatInvariant(product.Id), async o =>
            {
                o.ExpiresIn(TimeSpan.FromMinutes(10));

                var data  = new Dictionary <string, CombinationAvailabilityInfo>();
                var query = _db.ProductVariantAttributeCombinations
                            .AsNoTracking()
                            .Where(x => x.ProductId == product.Id);

                if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes)
                {
                    query = query.Where(x => !x.IsActive || (x.StockQuantity <= 0 && !x.AllowOutOfStockOrders));
                }
                else
                {
                    query = query.Where(x => !x.IsActive);
                }

                // Do not proceed if there are too many unavailable combinations.
                var unavailableCombinationsCount = await query.CountAsync();

                if (unavailableCombinationsCount <= _performanceSettings.MaxUnavailableAttributeCombinations)
                {
                    var pager = query.ToFastPager();

                    while ((await pager.ReadNextPageAsync <ProductVariantAttributeCombination>()).Out(out var combinations))
                    {
                        foreach (var combination in combinations)
                        {
                            var selection = new ProductVariantAttributeSelection(combination.RawAttributes);
                            if (selection.AttributesMap.Any())
                            {
                                // <ProductVariantAttribute.Id>:<ProductVariantAttributeValue.Id>[,...]
                                var valuesKeys = selection.AttributesMap
                                                 .OrderBy(x => x.Key)
                                                 .Select(x => $"{x.Key}:{string.Join(",", x.Value.OrderBy(y => y))}");

                                data[string.Join("-", valuesKeys)] = new CombinationAvailabilityInfo
                                {
                                    IsActive     = combination.IsActive,
                                    IsOutOfStock = combination.StockQuantity <= 0 && !combination.AllowOutOfStockOrders
                                };
                            }
                        }
                    }
                }

                return(data);
            });

            if (!unavailableCombinations.Any())
            {
                return(null);
            }

            using var pool = StringBuilderPool.Instance.Get(out var builder);
            var selectedValuesMap = selectedValues.ToMultimap(x => x.ProductVariantAttributeId, x => x);

            if (attributes == null || currentValue == null)
            {
                // Create key to test selectedValues.
                foreach (var kvp in selectedValuesMap.OrderBy(x => x.Key))
                {
                    Append(builder, kvp.Key, kvp.Value.Select(x => x.Id).Distinct());
                }
            }
            else
            {
                // Create key to test currentValue.
                foreach (var attribute in attributes.OrderBy(x => x.Id))
                {
                    IEnumerable <int> valueIds;

                    var selectedIds = selectedValuesMap.ContainsKey(attribute.Id)
                        ? selectedValuesMap[attribute.Id].Select(x => x.Id)
                        : null;

                    if (attribute.Id == currentValue.ProductVariantAttributeId)
                    {
                        // Attribute to be tested.
                        if (selectedIds != null && attribute.IsMultipleChoice)
                        {
                            // Take selected values and append current value.
                            valueIds = selectedIds.Append(currentValue.Id).Distinct();
                        }
                        else
                        {
                            // Single selection attribute -> take current value.
                            valueIds = new[] { currentValue.Id };
                        }
                    }
                    else
                    {
                        // Other attribute.
                        if (selectedIds != null)
                        {
                            // Take selected value(s).
                            valueIds = selectedIds;
                        }
                        else
                        {
                            // No selected value -> no unavailable combination.
                            return(null);
                        }
                    }

                    Append(builder, attribute.Id, valueIds);
                }
            }

            var key = builder.ToString();

            //$"{!unavailableCombinations.ContainsKey(key),-5} {currentValue.ProductVariantAttributeId}:{currentValue.Id} -> {key}".Dump();

            if (unavailableCombinations.TryGetValue(key, out var availability))
            {
                return(availability);
            }

            return(null);
Beispiel #10
0
        public virtual async Task <ProductVariantAttributeCombination> FindAttributeCombinationAsync(int productId, ProductVariantAttributeSelection selection)
        {
            if (productId == 0 || !(selection?.AttributesMap?.Any() ?? false))
            {
                return(null);
            }

            var combinations = await _db.ProductVariantAttributeCombinations
                               .AsNoTracking()
                               .AsCaching(ProductAttributesCacheDuration)
                               .Where(x => x.ProductId == productId)
                               .Select(x => new { x.Id, x.RawAttributes })
                               .ToListAsync();

            foreach (var combination in combinations)
            {
                if (selection.Equals(new ProductVariantAttributeSelection(combination.RawAttributes)))
                {
                    return(await _db.ProductVariantAttributeCombinations.FindByIdAsync(combination.Id));
                }
            }

            return(null);
        }
        public virtual async Task <ProductVariantAttributeCombination> FindAttributeCombinationAsync(int productId, ProductVariantAttributeSelection selection)
        {
            // TODO: (core) (important) Save combination hash in table and always lookup by hash instead of iterating thru local data to find a match.

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

            var cacheKey = ATTRIBUTECOMBINATION_BY_IDJSON_KEY.FormatInvariant(productId, selection.AsJson());

            var combination = await _requestCache.GetAsync(cacheKey, async() =>
            {
                var combinations = await _db.ProductVariantAttributeCombinations
                                   .AsNoTracking()
                                   .Where(x => x.ProductId == productId)
                                   .Select(x => new { x.Id, x.RawAttributes })
                                   .ToListAsync();

                foreach (var combination in combinations)
                {
                    if (selection.Equals(new ProductVariantAttributeSelection(combination.RawAttributes)))
                    {
                        return(await _db.ProductVariantAttributeCombinations.FindByIdAsync(combination.Id));
                    }
                }

                return(null);
            });

            return(combination);
        }