/// <summary> /// Gets a decimal array of the specified size with the value for the specified index set to 1. /// </summary> /// <param name="index">The index for the value that should be set to 1.</param> /// <param name="quantity">Item quantity.</param> /// <returns>The resulting decimal array.</returns> /// <remarks>For each line item, item quantity for discount application is 1 if line item quantity is integer and the whole quantity if fractional.</remarks> private static Dictionary <int, decimal> GetItemQuantitiesForDiscountApplication(int index, decimal quantity) { Dictionary <int, decimal> result = new Dictionary <int, decimal>(); if (DiscountableItemGroup.IsFraction(quantity)) { result[index] = Math.Abs(quantity); } else { result[index] = 1M; } return(result); }
/// <summary> /// Gets the sort value to use for the specified discount line and item group. /// </summary> /// <param name="discountLine">The discount line used for this application.</param> /// <param name="discountableItemGroup">The item group used for this application.</param> /// <returns>The sort value.</returns> private static decimal GetSortValue(RetailDiscountLine discountLine, DiscountableItemGroup discountableItemGroup) { switch ((DiscountOfferMethod)discountLine.DiscountMethod) { case DiscountOfferMethod.DiscountAmount: return(discountLine.DiscountAmount); case DiscountOfferMethod.DiscountPercent: return(discountableItemGroup.Price * discountLine.DiscountPercent); case DiscountOfferMethod.OfferPrice: return(discountableItemGroup.Price - discountLine.DiscountLinePercentOrValue - discountLine.OfferPrice); default: return(0); } }
/// <summary> /// Gets all of the possible applications of this discount to the specified transaction and line items. /// </summary> /// <param name="transaction">The transaction to consider for discounts.</param> /// <param name="discountableItemGroups">The valid sales line items on the transaction to consider.</param> /// <param name="remainingQuantities">The remaining quantities of each of the sales lines to consider.</param> /// <param name="priceContext">The pricing context to use.</param> /// <param name="appliedDiscounts">Applied discount application.</param> /// <param name="itemsWithOverlappingDiscounts">Items with overlapping discounts.</param> /// <param name="isInterrupted">A flag indicating whether it's interrupted for too many discount applications.</param> /// <returns>The possible permutations of line items that this discount can apply to, or an empty collection if this discount cannot apply.</returns> public override IEnumerable <DiscountApplication> GetDiscountApplications( SalesTransaction transaction, DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities, PriceContext priceContext, IEnumerable <AppliedDiscountApplication> appliedDiscounts, HashSet <int> itemsWithOverlappingDiscounts, out bool isInterrupted) { if (discountableItemGroups == null) { throw new ArgumentNullException("discountableItemGroups"); } if (remainingQuantities == null) { throw new ArgumentNullException("remainingQuantities"); } if (priceContext == null) { throw new ArgumentNullException("priceContext"); } List <DiscountApplication> discountAppliations = new List <DiscountApplication>(); isInterrupted = false; // Get the discount code to use for any discount lines, if one is required. string discountCodeUsed = this.GetDiscountCodeForDiscount(transaction); for (int x = 0; x < discountableItemGroups.Length; x++) { DiscountableItemGroup discountableItemGroup = discountableItemGroups[x]; HashSet <decimal> discountLineNumberSet; if (remainingQuantities[x] != 0M && this.ItemGroupIndexToDiscountLineNumberSetMap.TryGetValue(x, out discountLineNumberSet)) { bool hasOverlap = this.HasOverlap(x, itemsWithOverlappingDiscounts); foreach (decimal discountLineNumber in discountLineNumberSet) { RetailDiscountLine line = this.DiscountLines[discountLineNumber]; DiscountApplication result = new DiscountApplication(this, applyStandalone: !hasOverlap) { RetailDiscountLines = new List <RetailDiscountLineItem>(1) { new RetailDiscountLineItem(x, line) }, SortIndex = GetSortIndexForRetailDiscountLine(line), SortValue = GetSortValue(line, discountableItemGroup), DiscountCode = discountCodeUsed }; result.ItemQuantities.AddRange(GetItemQuantitiesForDiscountApplication(x, discountableItemGroup.Quantity)); discountAppliations.Add(result); } } } return(discountAppliations); }
public void ApplyDiscountLines(SalesTransaction transaction, bool isReturn) { ThrowIf.Null(transaction, "transaction"); if (!this.salesLines.Any()) { return; } // Keep track of available line items that have non-compoundable discounts applied to them. HashSet <int> availableLines = new HashSet <int>(); // Get the lines in reverse order, so that BOGO scenarios apply to the last item by default. for (int x = this.salesLines.Count - 1; x >= 0; x--) { availableLines.Add(x); } // Finish up exclusive or best price first. foreach (DiscountLineQuantity discountLineQuantity in this.discountLineQuantitiesNonCompounded) { this.ApplyOneDiscountLine(discountLineQuantity, transaction, availableLines, isReturn); } //// Discounts will be in order of non-compoundable first, then compoundable, we should maintain that order. //// In addition, for each concurrency, threhold is the last. bool reallocateDiscountAmountForCompoundedThresholdDiscountAmount = false; // Prepare offer id to discount amount dictionary for compounded threshold discount with amount off. Dictionary <string, decimal> offerIdToDiscountAmountDictionary = new Dictionary <string, decimal>(StringComparer.OrdinalIgnoreCase); Dictionary <string, int> offerIdToPriorityDictionary = new Dictionary <string, int>(StringComparer.OrdinalIgnoreCase); foreach (DiscountLineQuantity discount in this.discountLineQuantitiesCompounded) { if (discount.DiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.MixAndMatch && discount.DiscountLine.IsCompoundable) { reallocateDiscountAmountForCompoundedThresholdDiscountAmount = true; } if (reallocateDiscountAmountForCompoundedThresholdDiscountAmount && discount.DiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold && discount.DiscountLine.IsCompoundable && discount.DiscountLine.Amount > decimal.Zero) { decimal thresholdDiscountAmount = decimal.Zero; offerIdToDiscountAmountDictionary.TryGetValue(discount.DiscountLine.OfferId, out thresholdDiscountAmount); // Effective amount was calculated earlier in ThresholdDiscount.cs for threshold discount with amount off. offerIdToDiscountAmountDictionary[discount.DiscountLine.OfferId] = thresholdDiscountAmount + this.priceContext.CurrencyAndRoundingHelper.Round(discount.DiscountLine.Amount * discount.Quantity); if (!offerIdToPriorityDictionary.ContainsKey(discount.DiscountLine.OfferId)) { offerIdToPriorityDictionary.Add(discount.DiscountLine.OfferId, discount.DiscountLine.PricingPriorityNumber); } } else { if (!discount.DiscountLine.IsCompoundable || discount.DiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold) { this.ApplyOneDiscountLine(discount, transaction, availableLines, isReturn); } else { // See comment for method ApplyOneDiscountLineToCompoundedOnly. decimal quantityNotApplied = this.ApplyOneDiscountLineToCompoundedOnly(discount, transaction, availableLines, isReturn); if (quantityNotApplied > decimal.Zero) { this.ApplyOneDiscountLine(discount, transaction, availableLines, isReturn, quantityNotApplied); } } } } //// We need to reallocate amount off for compounded threshold discount amount in some cases. //// Earlier we calculate discount amount right for the item group as a whole, but we weren't able to allocate it properly. //// E.g. compounded mix and match discount of bug one get one 1c, and compounded threshold discount of $10. //// Transaction has a item of quantity 2, one get 1c deal price, the other one gets nothing. //// Threshold discount amount would like $5 for both items, but the effective amount for the 1st one is just 1c. //// As such, we need to reallocate threshold discount $10 (total), 1c to 1st item, and $9.99 to the 2nd one. if (reallocateDiscountAmountForCompoundedThresholdDiscountAmount) { foreach (KeyValuePair <string, decimal> offerIdDiscountAmountPair in offerIdToDiscountAmountDictionary) { // First, build item index to discounted price dictionary, exclusing threshold percentage off. Dictionary <int, decimal> itemIndexToDiscountedPriceDictionary = new Dictionary <int, decimal>(); availableLines.Clear(); decimal totalPrice = decimal.Zero; for (int x = this.salesLines.Count - 1; x >= 0; x--) { IList <DiscountLine> existingDiscountLines = this.salesLines[x].DiscountLines; decimal salesLineQuantity = Math.Abs(this.salesLines[x].Quantity); // Ignore non-compounded sales lines. if (salesLineQuantity != decimal.Zero && !existingDiscountLines.Where(p => !p.IsCompoundable).Any()) { decimal discountedPrice = this.Price; availableLines.Add(x); // non-threshold discount $-off first foreach (DiscountLine discountLineAmountOff in existingDiscountLines) { if (discountLineAmountOff.Amount > decimal.Zero && discountLineAmountOff.PeriodicDiscountType != PeriodicDiscountOfferType.Threshold) { discountedPrice -= discountLineAmountOff.Amount; } } // non-threshold discount %-off foreach (DiscountLine discountLinePercentOff in existingDiscountLines) { if (discountLinePercentOff.Amount == decimal.Zero && discountLinePercentOff.PeriodicDiscountType != PeriodicDiscountOfferType.Threshold) { discountedPrice -= discountedPrice * (discountLinePercentOff.Percentage / 100m); } } // threshold discount $-off foreach (DiscountLine discountLineAmountOff in existingDiscountLines) { if (discountLineAmountOff.Amount > decimal.Zero && discountLineAmountOff.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold) { discountedPrice -= discountLineAmountOff.Amount; } } discountedPrice = Math.Max(decimal.Zero, discountedPrice); itemIndexToDiscountedPriceDictionary[x] = discountedPrice; totalPrice += discountedPrice * salesLineQuantity; } } decimal offerDiscountAmount = offerIdDiscountAmountPair.Value; int priority = 0; offerIdToPriorityDictionary.TryGetValue(offerIdDiscountAmountPair.Key, out priority); int[] salesLineIndicesSorted = itemIndexToDiscountedPriceDictionary.Keys.ToArray(); SalesLineDiscountedPriceComparer comparer = new SalesLineDiscountedPriceComparer(itemIndexToDiscountedPriceDictionary, this.salesLines); Array.Sort(salesLineIndicesSorted, comparer.CompareNetPrice); for (int index = 0; index < salesLineIndicesSorted.Length; index++) { int salesLineIndex = salesLineIndicesSorted[index]; SalesLine salesLine = this.salesLines[salesLineIndex]; decimal salesLineQuantity = Math.Abs(salesLine.Quantity); decimal price = itemIndexToDiscountedPriceDictionary[salesLineIndex]; if (DiscountableItemGroup.IsFraction(salesLineQuantity)) { decimal myDiscountAmount = offerDiscountAmount * ((price * salesLineQuantity) / totalPrice); myDiscountAmount = this.priceContext.CurrencyAndRoundingHelper.Round(myDiscountAmount); decimal unitDiscountAmount = myDiscountAmount / salesLineQuantity; offerDiscountAmount -= myDiscountAmount; salesLine.DiscountLines.Add(NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLine.LineNumber, unitDiscountAmount, priority)); } else { if (index == (salesLineIndicesSorted.Length - 1)) { // Last one. decimal smallestAmount = PriceContextHelper.GetSmallestNonNegativeAmount(this.priceContext, Math.Min(offerDiscountAmount, price)); if (smallestAmount > decimal.Zero && salesLineQuantity > decimal.Zero) { int totalDiscountAmountInSmallestAmount = (int)(offerDiscountAmount / smallestAmount); int averageDiscountAmountRoundingDownInSmallestAmount = totalDiscountAmountInSmallestAmount / (int)salesLineQuantity; decimal quantityForHigherDiscountAmount = totalDiscountAmountInSmallestAmount - (averageDiscountAmountRoundingDownInSmallestAmount * (int)salesLineQuantity); decimal quantityForLowerDiscountAmount = salesLineQuantity - quantityForHigherDiscountAmount; decimal unitDiscountAmountForLowerDiscountAmount = averageDiscountAmountRoundingDownInSmallestAmount * smallestAmount; if (quantityForHigherDiscountAmount > decimal.Zero) { SalesLine salesLineToAddDiscount = salesLine; if (quantityForLowerDiscountAmount > decimal.Zero) { SalesLine newLine = this.SplitLine(salesLine, quantityForHigherDiscountAmount); // Add the new line to the transaction and to the available lines for discounts. // Set the line number to the next available number. newLine.LineNumber = transaction.SalesLines.Max(p => p.LineNumber) + 1; transaction.SalesLines.Add(newLine); this.salesLines.Add(newLine); salesLineToAddDiscount = newLine; } DiscountLine discountLine = NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLineToAddDiscount.LineNumber, unitDiscountAmountForLowerDiscountAmount + smallestAmount, priority); salesLineToAddDiscount.DiscountLines.Add(discountLine); } if (quantityForLowerDiscountAmount > decimal.Zero && unitDiscountAmountForLowerDiscountAmount > decimal.Zero) { DiscountLine discountLine = NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLine.LineNumber, unitDiscountAmountForLowerDiscountAmount, priority); salesLine.DiscountLines.Add(discountLine); } } } else { decimal myDiscountAmount = offerDiscountAmount * ((price * salesLineQuantity) / totalPrice); decimal unitDiscountAmount = this.priceContext.CurrencyAndRoundingHelper.Round(myDiscountAmount / salesLineQuantity); if (unitDiscountAmount > decimal.Zero) { offerDiscountAmount -= unitDiscountAmount * salesLineQuantity; salesLine.DiscountLines.Add(NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLine.LineNumber, unitDiscountAmount, priority)); } } } } } } }