private static void AddSelectedAttributes(this PriceCalculationContext context, ShoppingCartItem item)
 {
     if (item != null)
     {
         context.AddSelectedAttributes(item.AttributeSelection, item.ProductId, item.BundleItemId);
     }
 }
Ejemplo n.º 2
0
        public virtual async Task <(decimal Amount, Discount AppliedDiscount)> GetDiscountAmountAsync(
            Product product,
            Customer customer                = null,
            decimal additionalCharge         = decimal.Zero,
            int quantity                     = 1,
            ProductBundleItemData bundleItem = null,
            PriceCalculationContext context  = null,
            decimal?finalPrice               = null)
        {
            Guard.NotNull(product, nameof(product));

            customer ??= _workContext.CurrentCustomer;

            var      discountAmount  = decimal.Zero;
            Discount appliedDiscount = null;

            if (bundleItem != null && bundleItem.Item != null)
            {
                var bi = bundleItem.Item;
                if (bi.Discount.HasValue && bi.BundleProduct.BundlePerItemPricing)
                {
                    appliedDiscount = new Discount
                    {
                        UsePercentage      = bi.DiscountPercentage,
                        DiscountPercentage = bi.Discount.Value,
                        DiscountAmount     = bi.Discount.Value
                    };

                    var finalPriceWithoutDiscount = finalPrice ?? await GetFinalPriceAsync(product, customer, additionalCharge, false, quantity, bundleItem, context);

                    discountAmount = appliedDiscount.GetDiscountAmount(finalPriceWithoutDiscount);
                }
            }
            else
            {
                // Don't apply when customer entered price or discounts should be ignored in any case.
                if (!product.CustomerEntersPrice && _catalogSettings.IgnoreDiscounts)
                {
                    return(discountAmount, appliedDiscount);
                }

                var allowedDiscounts = await GetAllowedDiscountsAsync(product, customer, context);

                if (!allowedDiscounts.Any())
                {
                    return(discountAmount, appliedDiscount);
                }

                var finalPriceWithoutDiscount = finalPrice ?? await GetFinalPriceAsync(product, customer, additionalCharge, false, quantity, bundleItem, context);

                appliedDiscount = allowedDiscounts.GetPreferredDiscount(finalPriceWithoutDiscount);

                if (appliedDiscount != null)
                {
                    discountAmount = appliedDiscount.GetDiscountAmount(finalPriceWithoutDiscount);
                }
            }

            return(discountAmount, appliedDiscount);
        }
Ejemplo n.º 3
0
        public static void AddAttributes(this PriceCalculationContext context, ShoppingCartItem item)
        {
            Guard.NotNull(context, nameof(context));

            if (item != null)
            {
                context.AddAttributes(item.AttributeSelection, item.ProductId, item.BundleItemId);
            }
        }
Ejemplo n.º 4
0
 public virtual async Task <Money> GetProductVariantAttributeValuePriceAdjustmentAsync(
     ProductVariantAttributeValue attributeValue,
     Product product,
     Customer customer,
     PriceCalculationContext context,
     int quantity = 1)
 {
     return(new(await GetVariantPriceAdjustmentAsync(attributeValue, product, customer, context, quantity), _primaryCurrency));
 }
        protected PriceCalculationContext(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            Product            = context.Product;
            Quantity           = context.Quantity;
            Options            = context.Options;
            Metadata           = context.Metadata;
            AssociatedProducts = context.AssociatedProducts;
            // [...]
        }
        protected PriceCalculationContext(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            // For reasons of practicability, we only copy the data provided by the caller
            // but no intermediate data determined by the pipeline itself.
            Product            = context.Product;
            Quantity           = context.Quantity;
            Options            = context.Options;
            Metadata           = context.Metadata;
            SelectedAttributes = context.SelectedAttributes;
        }
        /// <summary>
        /// Adds selected product attributes to be taken into account in the price calculation.
        /// For example required for price adjustments of attributes selected by the customer.
        /// </summary>
        /// <param name="context">The target product calculation context.</param>
        /// <param name="selection">The selected product attributes.</param>
        /// <param name="productId">Product identifier.</param>
        /// <param name="bundleItemId">Bundle item identifier if the related product is a bundle item.</param>
        public static void AddSelectedAttributes(this PriceCalculationContext context, ProductVariantAttributeSelection selection, int productId, int?bundleItemId = null)
        {
            Guard.NotNull(context, nameof(context));

            if (selection?.AttributesMap?.Any() ?? false)
            {
                context.SelectedAttributes.Add(new PriceCalculationAttributes(selection, productId)
                {
                    BundleItemId = bundleItemId
                });
            }
        }
Ejemplo n.º 8
0
        public virtual async Task <decimal> GetFinalPriceAsync(
            Product product,
            IEnumerable <ProductBundleItemData> bundleItems,
            Customer customer                = null,
            decimal additionalCharge         = decimal.Zero,
            bool includeDiscounts            = true,
            int quantity                     = 1,
            ProductBundleItemData bundleItem = null,
            PriceCalculationContext context  = null)
        {
            Guard.NotNull(product, nameof(product));

            customer ??= _workContext.CurrentCustomer;

            if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)
            {
                var result = decimal.Zero;
                var items  = bundleItems;

                if (items == null)
                {
                    IEnumerable <ProductBundleItem> loadedBundleItems;

                    if (context == null)
                    {
                        loadedBundleItems = await _db.ProductBundleItem
                                            .AsNoTracking()
                                            .Include(x => x.Product)
                                            .ApplyBundledProductsFilter(new int[] { product.Id })
                                            .ToListAsync();
                    }
                    else
                    {
                        loadedBundleItems = await context.ProductBundleItems.GetOrLoadAsync(product.Id);
                    }

                    items = loadedBundleItems.Select(x => new ProductBundleItemData(x)).ToList();
                }

                foreach (var itemData in items.Where(x => x?.Item != null))
                {
                    var itemPrice = await GetFinalPriceAsync(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData, context);

                    result += decimal.Multiply(itemPrice, itemData.Item.Quantity);
                }

                return(result < decimal.Zero ? decimal.Zero : result);
            }

            return(await GetFinalPriceAsync(product, customer, additionalCharge, includeDiscounts, quantity, bundleItem, context));
        }
        protected PriceCalculationContext(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            Product            = context.Product;
            CartItem           = context.CartItem;
            AssociatedProducts = context.AssociatedProducts;
            BundleItems        = context.BundleItems;
            BundleItem         = context.BundleItem;
            Quantity           = context.Quantity;
            Options            = context.Options;
            Metadata           = context.Metadata;
            SelectedAttributes = context.SelectedAttributes;
        }
        public virtual async Task <CalculatedPrice> CalculatePriceAsync(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            // Remember source product.
            var product = context.Product;

            var calculatorContext = context.CartItem != null && product.CustomerEntersPrice
                ? new CalculatorContext(context, context.CartItem.Item.CustomerEnteredPrice)
                : await RunCalculators(context);

            var unitPrice = await CreateCalculatedPrice(calculatorContext, product);

            return(unitPrice);
        }
Ejemplo n.º 11
0
        protected PriceCalculationContext(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            Product            = context.Product;
            Quantity           = context.Quantity;
            Options            = context.Options;
            Metadata           = context.Metadata;
            AssociatedProducts = context.AssociatedProducts;
            BundleItems        = context.BundleItems;
            BundleItem         = context.BundleItem;
            Attributes         = context.Attributes;
            AdditionalCharge   = context.AdditionalCharge;
            // [...]
        }
        /// <summary>
        /// Adds selected product attributes of a shopping cart item to be taken into account in the price calculation.
        /// For example required for price adjustments of attributes selected by the customer.
        /// Also adds selected attributes of bundle items if <see cref="Product.BundlePerItemPricing"/> is activated.
        /// </summary>
        /// <param name="context">The target product calculation context.</param>
        /// <param name="item">Shopping cart item.</param>
        public static void AddSelectedAttributes(this PriceCalculationContext context, OrganizedShoppingCartItem cartItem)
        {
            Guard.NotNull(context, nameof(context));

            if (cartItem != null)
            {
                var item = cartItem.Item;

                context.AddSelectedAttributes(item);

                if (item.Product.ProductType == ProductType.BundledProduct && item.Product.BundlePerItemPricing)
                {
                    cartItem.ChildItems.Each(x => context.AddSelectedAttributes(x.Item));
                }
            }
        }
        private async Task <CalculatorContext> RunCalculators(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            // Remember source product.
            var product = context.Product;

            // Collect calculators
            var calculators       = _calculatorFactory.GetCalculators(context);
            var calculatorContext = new CalculatorContext(context, product.Price);

            // Run all collected calculators
            await _calculatorFactory.RunCalculators(calculators, calculatorContext);

            return(calculatorContext);
        }
Ejemplo n.º 14
0
        public static void AddAttributes(this PriceCalculationContext context, IEnumerable <OrganizedShoppingCartItem> cart)
        {
            Guard.NotNull(context, nameof(context));

            var item = cart?.FirstOrDefault(x => x.Item.ProductId == context.Product.Id);

            if (item?.Item != null)
            {
                context.AddAttributes(item.Item);

                if (item.Item.Product.ProductType == ProductType.BundledProduct && item.Item.Product.BundlePerItemPricing)
                {
                    item.ChildItems.Each(x => context.AddAttributes(x.Item));
                }
            }
        }
        /// <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>
        /// Gets the base price info for a product.
        /// </summary>
        /// <param name="priceCalculationService">Price calculation service.</param>
        /// <param name="product">The product to get the base price info for.</param>
        /// <param name="options">Price calculation options. The default options are used if <c>null</c>.</param>
        /// <returns>Base price info.</returns>
        public static async Task <string> GetBasePriceInfoAsync(this IPriceCalculationService priceCalculationService, Product product, PriceCalculationOptions options = null)
        {
            Guard.NotNull(priceCalculationService, nameof(priceCalculationService));
            Guard.NotNull(product, nameof(product));

            if (!product.BasePriceHasValue || product.BasePriceAmount == 0)
            {
                return(string.Empty);
            }

            options ??= priceCalculationService.CreateDefaultOptions(false);

            var context = new PriceCalculationContext(product, options);
            var price   = await priceCalculationService.CalculatePriceAsync(context);

            return(priceCalculationService.GetBasePriceInfo(product, price.FinalPrice, options.TargetCurrency));
        }
        public virtual async Task <PriceCalculationContext> CreateCalculationContextAsync(OrganizedShoppingCartItem cartItem, PriceCalculationOptions options)
        {
            Guard.NotNull(cartItem, nameof(cartItem));
            Guard.NotNull(options, nameof(options));

            var product = cartItem.Item.Product;
            var context = new PriceCalculationContext(product, cartItem.Item.Quantity, options)
            {
                CartItem = cartItem
            };

            // Include attributes selected for this cart item in price calculation.
            context.AddSelectedAttributes(cartItem);

            // Include bundle item data if the cart item is a bundle item.
            if (cartItem.Item.BundleItem != null)
            {
                context.BundleItem = cartItem.Item.BundleItem;
            }

            // Perf: we already have the bundle items of a bundled product. No need to load them again during calculation.
            if (cartItem.ChildItems?.Any() ?? false)
            {
                context.BundleItems = cartItem.ChildItems
                                      .Where(x => x.Item.BundleItem != null)
                                      .Select(x => x.Item.BundleItem)
                                      .ToList();
            }

            if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)
            {
                Guard.NotNull(cartItem.ChildItems, nameof(cartItem.ChildItems));

                foreach (var bundleItem in cartItem.ChildItems)
                {
                    await _productAttributeMaterializer.MergeWithCombinationAsync(bundleItem.Item.Product, bundleItem.Item.AttributeSelection);
                }
            }
            else
            {
                await _productAttributeMaterializer.MergeWithCombinationAsync(product, cartItem.Item.AttributeSelection);
            }

            return(context);
        }
Ejemplo n.º 18
0
        protected virtual bool MatchCalculator(PriceCalculationContext context, PriceCalculatorMetadata metadata)
        {
            var productType = context.Product.ProductType;

            if (productType == ProductType.SimpleProduct)
            {
                return(metadata.ValidTargets.HasFlag(CalculatorTargets.Product));
            }
            else if (productType == ProductType.BundledProduct)
            {
                return(metadata.ValidTargets.HasFlag(CalculatorTargets.Bundle));
            }
            else if (productType == ProductType.GroupedProduct)
            {
                return(metadata.ValidTargets.HasFlag(CalculatorTargets.GroupedProduct));
            }

            return(false);
        }
Ejemplo n.º 19
0
        public virtual async Task <(Money?LowestPrice, Product LowestPriceProduct)> GetLowestPriceAsync(
            Product product,
            Customer customer,
            PriceCalculationContext context,
            IEnumerable <Product> associatedProducts)
        {
            Guard.NotNull(product, nameof(product));
            Guard.NotNull(associatedProducts, nameof(associatedProducts));

            if (product.ProductType != ProductType.GroupedProduct)
            {
                throw Error.InvalidOperation("Choose the other override for products not of type grouped product.");
            }

            decimal?lowestPrice        = null;
            Product lowestPriceProduct = null;

            context ??= new PriceCalculationContext(null, _services, _storeContext.CurrentStore, customer ?? _workContext.CurrentCustomer, true);

            foreach (var associatedProduct in associatedProducts)
            {
                var tmpPrice = await GetFinalPriceAmountAsync(associatedProduct, decimal.Zero, customer, true, int.MaxValue, null, context);

                if (associatedProduct.LowestAttributeCombinationPrice.HasValue && associatedProduct.LowestAttributeCombinationPrice.Value < tmpPrice)
                {
                    tmpPrice = associatedProduct.LowestAttributeCombinationPrice.Value;
                }

                if (!lowestPrice.HasValue || tmpPrice < lowestPrice.Value)
                {
                    lowestPrice        = tmpPrice;
                    lowestPriceProduct = associatedProduct;
                }
            }

            if (lowestPriceProduct == null)
            {
                lowestPriceProduct = associatedProducts.FirstOrDefault();
            }

            return(lowestPrice.HasValue ? new(lowestPrice.Value, _primaryCurrency) : null, lowestPriceProduct);
        }
Ejemplo n.º 20
0
 public virtual async Task <Money> GetFinalPriceAsync(
     Product product,
     IEnumerable <ProductBundleItemData> bundleItems,
     Money?additionalCharge,
     Customer customer                = null,
     bool includeDiscounts            = true,
     int quantity                     = 1,
     ProductBundleItemData bundleItem = null,
     PriceCalculationContext context  = null)
 {
     return(new(await GetFinalPriceAmountAsync(
                    product,
                    bundleItems,
                    additionalCharge?.Amount ?? decimal.Zero,
                    customer,
                    includeDiscounts,
                    quantity,
                    bundleItem,
                    context), _primaryCurrency));
 }
Ejemplo n.º 21
0
        public virtual async Task <(Money Amount, Discount AppliedDiscount)> GetDiscountAmountAsync(
            Product product,
            Money?additionalCharge,
            Customer customer = null,
            int quantity      = 1,
            ProductBundleItemData bundleItem = null,
            PriceCalculationContext context  = null,
            Money?finalPrice = null)
        {
            var(discountAmount, appliedDiscount) = await GetDiscountAmountAsync(
                product,
                additionalCharge?.Amount ?? decimal.Zero,
                customer,
                quantity,
                bundleItem,
                context,
                finalPrice?.Amount);

            return(new(discountAmount, _primaryCurrency), appliedDiscount);
        }
Ejemplo n.º 22
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));
        }
        public virtual async Task <Money> GetProductVariantAttributeValuePriceAdjustmentAsync(
            ProductVariantAttributeValue attributeValue,
            Product product,
            Customer customer,
            PriceCalculationContext context,
            int quantity = 1)
        {
            Guard.NotNull(attributeValue, nameof(attributeValue));

            var currency = _workContext.WorkingCurrency;

            if (attributeValue.ValueType == ProductVariantAttributeValueType.Simple)
            {
                if (quantity > 1 && attributeValue.PriceAdjustment > 0)
                {
                    var tierPriceAttributeAdjustment = await GetTierPriceAttributeAdjustmentAsync(product, customer, quantity, context, attributeValue.PriceAdjustment);

                    if (tierPriceAttributeAdjustment != 0)
                    {
                        return(tierPriceAttributeAdjustment);
                    }
                }

                return(new Money(attributeValue.PriceAdjustment, currency));
            }

            if (attributeValue.ValueType == ProductVariantAttributeValueType.ProductLinkage)
            {
                var linkedProduct = await _db.Products.FindByIdAsync(attributeValue.LinkedProductId);

                if (linkedProduct != null)
                {
                    var productPrice = await GetFinalPriceAsync(linkedProduct, null) * attributeValue.Quantity;

                    return(productPrice);
                }
            }

            return(new Money(currency));
        }
Ejemplo n.º 24
0
        public IPriceCalculator[] GetCalculators(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            if (context.Calculators != null && context.Calculators.Length > 0)
            {
                // Don't resolve calculators if context has a specific calculators list.
                return(context.Calculators);
            }

            var cacheKey = "PriceCalculators:" + GenerateHashCode(context).ToString();

            var calculators = _requestCache.Get(cacheKey, () =>
            {
                return(_lazyCalculators
                       .Where(x => MatchCalculator(context, x.Metadata))
                       .Select(x => x.Value)
                       .ToArray());
            });

            return(calculators);
        }
Ejemplo n.º 25
0
        protected virtual async Task <decimal> GetVariantPriceAdjustmentAsync(
            ProductVariantAttributeValue attributeValue,
            Product product,
            Customer customer,
            PriceCalculationContext context,
            int quantity = 1)
        {
            Guard.NotNull(attributeValue, nameof(attributeValue));

            if (attributeValue.ValueType == ProductVariantAttributeValueType.Simple)
            {
                if (quantity > 1 && attributeValue.PriceAdjustment > decimal.Zero)
                {
                    var tierPriceAttributeAdjustment = await GetTierPriceAttributeAdjustmentAsync(product, customer, quantity, context, attributeValue.PriceAdjustment);

                    if (tierPriceAttributeAdjustment != decimal.Zero)
                    {
                        return(tierPriceAttributeAdjustment);
                    }
                }

                return(attributeValue.PriceAdjustment);
            }
            else if (attributeValue.ValueType == ProductVariantAttributeValueType.ProductLinkage)
            {
                var linkedProduct = await _db.Products.FindByIdAsync(attributeValue.LinkedProductId);

                if (linkedProduct != null)
                {
                    var productPrice = await GetFinalPriceAmountAsync(linkedProduct, decimal.Zero, includeDiscounts : true);

                    return(productPrice * attributeValue.Quantity);
                }
            }

            return(decimal.Zero);
        }
        public virtual async Task <(CalculatedPrice UnitPrice, CalculatedPrice Subtotal)> CalculateSubtotalAsync(PriceCalculationContext context)
        {
            Guard.NotNull(context, nameof(context));

            // Remember source product.
            var product = context.Product;

            var calculatorContext = context.CartItem != null && product.CustomerEntersPrice
                ? new CalculatorContext(context, context.CartItem.Item.CustomerEnteredPrice)
                : await RunCalculators(context);

            var unitPrice = await CreateCalculatedPrice(calculatorContext, product);

            if (context.Quantity > 1)
            {
                var options  = context.Options;
                var subtotal = await CreateCalculatedPrice(calculatorContext, product, context.Quantity);

                // Avoid rounding differences between unit price and line subtotal when calculating with net prices.
                // ... but this produces a rounding difference between subtotal and line subtotals which can only be solved by a hack.
                var priceAmount = options.RoundingCurrency.RoundIfEnabledFor(unitPrice.FinalPrice.Amount) * context.Quantity;
                subtotal.FinalPrice = new(priceAmount, unitPrice.FinalPrice.Currency);

                return(unitPrice, subtotal);
            }

            return(unitPrice, unitPrice);

            //var subtotal = context.Quantity > 1
            //    ? await CreateCalculatedPrice(calculatorContext, product, context.Quantity)
            //    : unitPrice;

            //return (unitPrice, subtotal);
        }
Ejemplo n.º 27
0
 public CalculatorContext(PriceCalculationContext context, decimal regularPrice)
     : base(context)
 {
     RegularPrice = regularPrice;
     FinalPrice   = regularPrice;
 }
Ejemplo n.º 28
0
        protected virtual async Task <decimal> GetFinalPriceAmountAsync(
            Product product,
            decimal additionalCharge,
            Customer customer                = null,
            bool includeDiscounts            = true,
            int quantity                     = 1,
            ProductBundleItemData bundleItem = null,
            PriceCalculationContext context  = null,
            bool isTierPrice                 = false)
        {
            Guard.NotNull(product, nameof(product));

            customer ??= _workContext.CurrentCustomer;

            if (isTierPrice)
            {
                includeDiscounts = true;
            }

            var result = GetSpecialPriceAmount(product) ?? product.Price;

            // Tier price.
            if (product.HasTierPrices && includeDiscounts && !(bundleItem != null && bundleItem.Item != null))
            {
                var tierPrice = await GetMinimumTierPriceAsync(product, customer, quantity, context);

                if (tierPrice.HasValue)
                {
                    if (_catalogSettings.ApplyPercentageDiscountOnTierPrice && !isTierPrice)
                    {
                        var(discountOnTierPrice, appliedDiscount) = await GetDiscountAmountAsync(product, decimal.Zero, customer, quantity, bundleItem, context, tierPrice.Value);

                        if (appliedDiscount != null && appliedDiscount.UsePercentage)
                        {
                            result = Math.Min(result, tierPrice.Value) + additionalCharge - discountOnTierPrice;

                            return(Math.Max(result, decimal.Zero));
                        }
                    }

                    var(discountAmountTest, _) = await GetDiscountAmountAsync(product, additionalCharge, customer, quantity, bundleItem);

                    var discountProductTest = result - discountAmountTest;

                    if (tierPrice < discountProductTest)
                    {
                        includeDiscounts = false;
                        result           = Math.Min(result, tierPrice.Value);
                    }
                }
            }

            result += additionalCharge;

            if (includeDiscounts)
            {
                var(discountAmount, _) = await GetDiscountAmountAsync(product, additionalCharge, customer, quantity, bundleItem, context);

                result -= discountAmount;
            }

            return(Math.Max(result, decimal.Zero));
        }
Ejemplo n.º 29
0
        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);
        }
Ejemplo n.º 30
0
        protected virtual async Task <decimal> GetTierPriceAttributeAdjustmentAsync(Product product, Customer customer, int quantity, PriceCalculationContext context = null, decimal adjustment = 0)
        {
            var result     = decimal.Zero;
            var tierPrices = await LoadTierPrices(product, customer, context);

            if (tierPrices.Any())
            {
                var previousQty = 1;

                foreach (var tierPrice in tierPrices)
                {
                    if (quantity < tierPrice.Quantity || tierPrice.Quantity < previousQty)
                    {
                        continue;
                    }

                    if (tierPrice.CalculationMethod == TierPriceCalculationMethod.Percental && _catalogSettings.ApplyTierPricePercentageToAttributePriceAdjustments)
                    {
                        result = adjustment - (adjustment / 100m * tierPrice.Price);
                    }

                    previousQty = tierPrice.Quantity;
                }
            }

            return(result);
        }