/// <summary> /// Gets the final price for a child product by running a nested calculation pipeline. /// </summary> /// <param name="childProduct"> /// The child product (e.g. associated product of a grouped product or a bundle item part) to calculate price for. /// </param> /// <param name="context">The calculator context of the root pipeline.</param> /// <param name="childContextConfigurer">An optional configurer action for the child context.</param> /// <returns>The nested calculator context.</returns> protected async Task <CalculatorContext> CalculateChildPriceAsync(Product childProduct, CalculatorContext context, Action <CalculatorContext> childContextConfigurer = null) { if (context.Product == childProduct) { // TODO: (core) Decent error message for price calculation deadlocking. throw new InvalidOperationException("Deadlock"); } var childCalculatorContext = new CalculatorContext(context, childProduct.Price) { Product = childProduct }; childContextConfigurer?.Invoke(childCalculatorContext); // INFO: we know that options have been cloned. if (context.Options.ChildProductsBatchContext != null) { childCalculatorContext.Options.BatchContext = context.Options.ChildProductsBatchContext; } // Get calculators for child product context var calculators = _calculatorFactory.GetCalculators(childCalculatorContext); // Run calculators await _calculatorFactory.RunCalculators(calculators, childCalculatorContext); return(childCalculatorContext); }
/// <summary> /// Gets the final price for a child product by running a nested calculation pipeline. /// </summary> /// <param name="childProduct"> /// The child product (e.g. associated product of a grouped product or a bundle item part) to calculate price for. /// Must not be the same product as <see cref="PriceCalculationContext.Product"/>. /// </param> /// <param name="context">The calculator context of the root pipeline.</param> /// <param name="childContextConfigurer">An optional configurer action for the child context.</param> /// <returns>The nested calculator context.</returns> protected async Task <CalculatorContext> CalculateChildPriceAsync(Product childProduct, CalculatorContext context, Action <CalculatorContext> childContextConfigurer = null) { if (context.Product == childProduct) { throw new InvalidOperationException("The product of a nested calculation pipeline cannot be the same as that of the root pipeline. It would result in a deadlock."); } var childCalculatorContext = new CalculatorContext(context, childProduct.Price) { Product = childProduct }; childContextConfigurer?.Invoke(childCalculatorContext); // INFO: we know that options have been cloned. if (context.Options.ChildProductsBatchContext != null) { childCalculatorContext.Options.BatchContext = context.Options.ChildProductsBatchContext; } // Get calculators for child product context var calculators = _calculatorFactory.GetCalculators(childCalculatorContext); // Run calculators await _calculatorFactory.RunCalculators(calculators, childCalculatorContext); return(childCalculatorContext); }
public async Task RunCalculators(IPriceCalculator[] calculators, CalculatorContext context) { Guard.NotNull(calculators, nameof(calculators)); Guard.NotNull(context, nameof(context)); var numCalculators = calculators.Length; if (numCalculators == 0) { return; } // Start the pipeline with first calculator middleware int i = 0; await Next(context); async Task Next(CalculatorContext ctx) { if (i >= numCalculators) { return; } i++; await calculators[i - 1].CalculateAsync(ctx, Next); } }
/// <summary> /// Gets the final price for a child product by running a nested calculation pipeline. /// </summary> /// <returns>The nested calculator context.</returns> protected async Task <CalculatorContext> CalculateChildPriceAsync(Product childProduct, CalculatorContext context) { if (context.Product == childProduct) { // TODO: (core) Decent error message for price calculation deadlocking. throw new InvalidOperationException("Deadlock"); } var childCalculatorContext = new CalculatorContext(context, childProduct.Price) { Product = childProduct, Quantity = int.MaxValue }; // INFO: we know that options have been cloned. childCalculatorContext.Options.BatchContext = context.Options.ChildProductsBatchContext; childCalculatorContext.Options.IgnoreGroupedProducts = true; childCalculatorContext.Options.IgnoreBundles = true; // Get calculators for child product context var calculators = _calculatorFactory.GetCalculators(childCalculatorContext); // Run calculators await _calculatorFactory.RunCalculators(calculators, childCalculatorContext); return(childCalculatorContext); }
public CalculatedPrice(CalculatorContext context) { Guard.NotNull(context, nameof(context)); Product = context.Product; AppliedDiscounts = context.AppliedDiscounts; HasPriceRange = context.HasPriceRange; AttributePriceAdjustments = context.AttributePriceAdjustments; }
public CalculatedPrice(CalculatorContext context) { Guard.NotNull(context, nameof(context)); Product = context.Product; AppliedDiscounts = context.AppliedDiscounts; //AppliedTierPrice = context.AppliedTierPrice; //context.AppliedAttributeCombination = context.AppliedAttributeCombination; HasPriceRange = context.HasPriceRange; }
public void CopyTo(CalculatorContext target) { Guard.NotNull(target, nameof(target)); target.Product = Product; target.RegularPrice = RegularPrice; target.OfferPrice = OfferPrice; target.PreselectedPrice = FinalPrice; target.FinalPrice = FinalPrice; target.HasPriceRange = HasPriceRange; target.AppliedDiscounts.Clear(); target.AppliedDiscounts.AddRange(AppliedDiscounts); }
private Money?ConvertAmount(decimal?amount, CalculatorContext context, TaxRate taxRate, bool isFinalPrice, out Tax?tax) { tax = null; if (amount == null) { return(null); } var options = context.Options; // A product price cannot be less than zero. if (amount < 0) { amount = 0; } if (amount != 0) { tax = options.IsGrossPrice ? _taxCalculator.CalculateTaxFromGross(amount.Value, taxRate, options.TaxInclusive, options.RoundingCurrency) : _taxCalculator.CalculateTaxFromNet(amount.Value, taxRate, options.TaxInclusive, options.RoundingCurrency); amount = tax.Value.Price; } var money = _currencyService.ConvertFromPrimaryCurrency(amount.Value, options.TargetCurrency); if (amount != 0 && options.TaxFormat != null) { money = money.WithPostFormat(options.TaxFormat); } if (isFinalPrice && context.HasPriceRange) { var finalPricePostFormat = money.PostFormat; finalPricePostFormat = finalPricePostFormat == null ? options.PriceRangeFormat : string.Format(options.PriceRangeFormat, finalPricePostFormat); if (money.PostFormat != finalPricePostFormat) { money = money.WithPostFormat(finalPricePostFormat); } } return(money); }
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); }
private async Task <CalculatedPrice> CreateCalculatedPrice(CalculatorContext context, Product product = null, int subtotalQuantity = 1) { product ??= context.Product; var options = context.Options; // Calculate the subtotal price instead of the unit price. if (subtotalQuantity > 1 && context.FinalPrice > 0) { context.FinalPrice = options.RoundingCurrency.RoundIfEnabledFor(context.FinalPrice) * subtotalQuantity; context.DiscountAmount = options.RoundingCurrency.RoundIfEnabledFor(context.DiscountAmount) * subtotalQuantity; } // Determine tax rate for product. var taxRate = await _taxService.GetTaxRateAsync(product, null, options.Customer); // Prepare result by converting price amounts. var result = new CalculatedPrice(context) { Product = product, RegularPrice = ConvertAmount(context.RegularPrice, context, taxRate, false, out _).Value, OfferPrice = ConvertAmount(context.OfferPrice, context, taxRate, false, out _), PreselectedPrice = ConvertAmount(context.PreselectedPrice, context, taxRate, false, out _), LowestPrice = ConvertAmount(context.LowestPrice, context, taxRate, false, out _), DiscountAmount = ConvertAmount(context.DiscountAmount, context, taxRate, false, out _).Value, FinalPrice = ConvertAmount(context.FinalPrice, context, taxRate, true, out var tax).Value, Tax = tax }; if (tax.HasValue && _primaryCurrency != options.TargetCurrency) { // Exchange tax amounts. // TODO: (mg) (core) Check for rounding issues thoroughly! result.Tax = new Tax( tax.Value.Rate, // Amount _currencyService.ConvertFromPrimaryCurrency(tax.Value.Amount, options.TargetCurrency).Amount, // Price result.FinalPrice.Amount, tax.Value.IsGrossPrice, tax.Value.Inclusive); } // Convert attribute price adjustments. context.AttributePriceAdjustments.Each(x => x.Price = ConvertAmount(x.RawPriceAdjustment, context, taxRate, false, out _).Value); // Calculate price saving. // The final price without discounts has priority over the old price. // This avoids differing percentage discount in product lists and detail page. var priceWithoutDiscount = result.FinalPrice + result.DiscountAmount; var savingPrice = result.FinalPrice < priceWithoutDiscount ? priceWithoutDiscount : ConvertAmount(product.OldPrice, context, taxRate, false, out _).Value; var hasSaving = savingPrice > 0 && result.FinalPrice < savingPrice; result.PriceSaving = new PriceSaving { HasSaving = hasSaving, SavingPrice = savingPrice, SavingPercent = hasSaving ? (float)((savingPrice - result.FinalPrice) / savingPrice) * 100 : 0f, SavingAmount = hasSaving ? (savingPrice - result.FinalPrice).WithPostFormat(null) : null }; return(result); }
protected virtual void Calculate(CalculatorContext context) { }
public virtual async Task CalculateAsync(CalculatorContext context, CalculatorDelegate next) { Calculate(context); await next(context); }