internal bool IsOkayForMixAndMatchLeastExpensiveOneLineGroupPartialOptimizationAndFillupValueLookups( Dictionary <int, decimal> mixAndMatchRelativeValueLookup, List <int> itemGroupIndexListSortedByRelativePriceDescending, List <HashSet <int> > consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending, Dictionary <int, OfferDiscount> simpleDiscountOfferLookup, DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities) { // Special optimization for this overlapped group when there is // only one mix and match // with only one line group // with least expensive deal price and %-off bool isOkay = this.MixAndMatchAndQuantityDiscounts.Count == 1; consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending.Clear(); MixAndMatchDiscount mixAndMatchDiscount = null; if (isOkay) { mixAndMatchDiscount = this.MixAndMatchAndQuantityDiscounts.First().Value as MixAndMatchDiscount; isOkay = mixAndMatchDiscount != null; } if (isOkay) { isOkay = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.Count == 1; } if (isOkay) { isOkay = mixAndMatchDiscount.DiscountType == DiscountMethodType.LeastExpensive && !mixAndMatchDiscount.IsLeastExpensiveAmountOff; } if (isOkay) { var lineGroupNumberOfItemsPair = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.First(); string lineGroup = lineGroupNumberOfItemsPair.Key; decimal quantityNeeded = lineGroupNumberOfItemsPair.Value; HashSet <int> mixAndMatchItemIndexGroupSet = null; if (mixAndMatchDiscount.LineGroupToItemGroupIndexSetLookup.TryGetValue(lineGroup, out mixAndMatchItemIndexGroupSet)) { foreach (int itemGroupIndex in mixAndMatchItemIndexGroupSet) { if (remainingQuantities[itemGroupIndex] > decimal.Zero) { // Move it to mix and match decimal mixAndMatchValue = decimal.Zero; decimal price = discountableItemGroups[itemGroupIndex].Price; decimal totalPriceForDiscount = price * mixAndMatchDiscount.NumberOfLeastExpensiveLines; if (mixAndMatchDiscount.DiscountPercentValue > decimal.Zero) { mixAndMatchValue = DiscountBase.GetDiscountAmountForPercentageOff(totalPriceForDiscount, mixAndMatchDiscount.DiscountPercentValue); } else if (mixAndMatchDiscount.DiscountAmountValue == decimal.Zero && mixAndMatchDiscount.DealPriceValue > decimal.Zero) { mixAndMatchValue = totalPriceForDiscount - mixAndMatchDiscount.DealPriceValue; if (mixAndMatchValue < decimal.Zero) { isOkay = false; } } if (!isOkay) { break; } decimal simpleDiscountValue = decimal.Zero; int discountOfferCount = 0; // If an item has compounded discounts only, then we have already processed compounded discount offers, so we won't see them here. // And this won't affect algorithm. // If we modify optimization before overlapped discounts, then this may not be true. // See DiscountCalcuator.ReduceOverlappedOfferAndQuantityDiscountsPerItem. foreach (KeyValuePair <string, DiscountBase> pair in this.OfferDiscounts) { OfferDiscount offer = pair.Value as OfferDiscount; if (offer != null) { HashSet <decimal> discountLineNumberSetForItem = null; if (offer.ItemGroupIndexToDiscountLineNumberSetMap.TryGetValue(itemGroupIndex, out discountLineNumberSetForItem)) { if (discountLineNumberSetForItem.Count >= 1) { decimal discountLineNumber = discountLineNumberSetForItem.First(); RetailDiscountLine discountLineDefinition = offer.DiscountLines[discountLineNumber]; // For now, it works for fully covered items. // Fully covered, as an example, buy 3 for $10, then if item price is $3, it won't get discount // or buy 3 for $5 off, then if item price is $1, it won't get full discount. bool isFullyCovered = false; simpleDiscountValue = OfferDiscount.GetUnitDiscountAmountAndCheckWhetherItsFullyCovered(discountLineDefinition, price, out isFullyCovered) * quantityNeeded; if (!isFullyCovered) { isOkay = false; } if (!isOkay) { break; } simpleDiscountOfferLookup[itemGroupIndex] = offer; discountOfferCount++; } } } } // For now, we don't handle multiple discount offers. // With optimization earlier, that's mostly the case. See DiscountCalcuator.ReduceOverlappedOfferAndQuantityDiscountsPerItem. if (discountOfferCount > 1) { isOkay = false; } if (!isOkay) { break; } mixAndMatchRelativeValueLookup.Add(itemGroupIndex, mixAndMatchValue - simpleDiscountValue); } } } } if (isOkay) { // Now group items by relative price and price, and order them by relative price, in consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending. // See tests in OverlappedDiscountsUnitTests. itemGroupIndexListSortedByRelativePriceDescending.AddRange(mixAndMatchRelativeValueLookup.Keys); ItemPriceComparer itemPriceComparer = new ItemPriceComparer(mixAndMatchRelativeValueLookup); itemGroupIndexListSortedByRelativePriceDescending.Sort(itemPriceComparer.GetComparison()); decimal previousRelativePrice = decimal.Zero; Dictionary <decimal, HashSet <int> > itemPriceToItemGroupIndexSetLookup = new Dictionary <decimal, HashSet <int> >(); for (int listIndex = 0; listIndex < itemGroupIndexListSortedByRelativePriceDescending.Count; listIndex++) { int itemGroupIndex = itemGroupIndexListSortedByRelativePriceDescending[listIndex]; decimal currentPrice = discountableItemGroups[itemGroupIndex].Price; decimal currentRelativePrice = mixAndMatchRelativeValueLookup[itemGroupIndex]; if (listIndex == 0) { previousRelativePrice = currentRelativePrice; itemPriceToItemGroupIndexSetLookup.Add(currentPrice, new HashSet <int>() { itemGroupIndex }); } else { if (currentRelativePrice == previousRelativePrice) { HashSet <int> itemGroupIndexSetWithSameRelativePriceAndPrice = null; if (itemPriceToItemGroupIndexSetLookup.TryGetValue(currentPrice, out itemGroupIndexSetWithSameRelativePriceAndPrice)) { itemGroupIndexSetWithSameRelativePriceAndPrice.Add(itemGroupIndex); } else { itemPriceToItemGroupIndexSetLookup.Add(currentPrice, new HashSet <int>() { itemGroupIndex }); } } if (currentRelativePrice != previousRelativePrice) { foreach (KeyValuePair <decimal, HashSet <int> > pair in itemPriceToItemGroupIndexSetLookup) { HashSet <int> itemGroupIndexSetWithSameRelativePriceAndPrice = pair.Value; consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending.Add(new HashSet <int>(itemGroupIndexSetWithSameRelativePriceAndPrice)); } previousRelativePrice = currentRelativePrice; itemPriceToItemGroupIndexSetLookup.Clear(); itemPriceToItemGroupIndexSetLookup.Add(currentPrice, new HashSet <int>() { itemGroupIndex }); } if (listIndex == itemGroupIndexListSortedByRelativePriceDescending.Count - 1) { foreach (KeyValuePair <decimal, HashSet <int> > pair in itemPriceToItemGroupIndexSetLookup) { HashSet <int> itemGroupIndexSetWithSameRelativePriceAndPrice = pair.Value; consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending.Add(new HashSet <int>(itemGroupIndexSetWithSameRelativePriceAndPrice)); } itemPriceToItemGroupIndexSetLookup.Clear(); } } } } return(isOkay); }
internal bool IsOkayForMixAndMatchOneLineGroupOptimizationAndFillupValueLookups( Dictionary <int, decimal> mixAndMatchRelativeValueLookup, Dictionary <int, OfferDiscount> simpleDiscountOfferLookup, DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities) { // Special optimization for this overlapped group when there is // only one mix and match // with only one line group // with any of deal price, $ off, % off, or least expensive $-off. bool isOkay = this.MixAndMatchAndQuantityDiscounts.Count == 1; MixAndMatchDiscount mixAndMatchDiscount = null; if (isOkay) { mixAndMatchDiscount = this.MixAndMatchAndQuantityDiscounts.First().Value as MixAndMatchDiscount; isOkay = mixAndMatchDiscount != null; } if (isOkay) { isOkay = mixAndMatchDiscount.DiscountType == DiscountMethodType.DealPrice || mixAndMatchDiscount.DiscountType == DiscountMethodType.DiscountPercent || mixAndMatchDiscount.DiscountType == DiscountMethodType.DiscountAmount || mixAndMatchDiscount.IsLeastExpensiveAmountOff; } if (isOkay) { isOkay = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.Count == 1; } if (isOkay) { // Mix and match example: buy 3 for $10. // For each item, calculate relative value of mix and match against discount offer with quantity 3 as a group. // relative value of item A = M(AAA) - 3S(A) // See DiscountCalculator.TryOptimizeForOneMixAndMatchWithOneLineGroup var lineGroupNumberOfItemsPair = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.First(); string lineGroup = lineGroupNumberOfItemsPair.Key; decimal quantityNeeded = lineGroupNumberOfItemsPair.Value; HashSet <int> mixAndMatchItemIndexGroupSet = null; if (mixAndMatchDiscount.LineGroupToItemGroupIndexSetLookup.TryGetValue(lineGroup, out mixAndMatchItemIndexGroupSet)) { foreach (int itemGroupIndex in mixAndMatchItemIndexGroupSet) { if (remainingQuantities[itemGroupIndex] > decimal.Zero) { // Move it to mix and match decimal mixAndMatchValue = decimal.Zero; decimal price = discountableItemGroups[itemGroupIndex].Price; decimal totalPrice = price * quantityNeeded; switch (mixAndMatchDiscount.DiscountType) { case DiscountMethodType.DealPrice: mixAndMatchValue = totalPrice - mixAndMatchDiscount.DealPriceValue; isOkay = mixAndMatchValue >= decimal.Zero; break; case DiscountMethodType.DiscountAmount: mixAndMatchValue = mixAndMatchDiscount.DiscountAmountValue; isOkay = totalPrice >= mixAndMatchDiscount.DiscountAmountValue; break; case DiscountMethodType.DiscountPercent: mixAndMatchValue = DiscountBase.GetDiscountAmountForPercentageOff(totalPrice, mixAndMatchDiscount.DiscountPercentValue); break; default: isOkay = false; if (mixAndMatchDiscount.IsLeastExpensiveAmountOff && price >= mixAndMatchDiscount.DiscountAmountValue) { isOkay = true; mixAndMatchValue = mixAndMatchDiscount.DiscountAmountValue; } break; } if (!isOkay) { break; } int discountOfferCount = 0; decimal simpleDiscountValue = decimal.Zero; foreach (KeyValuePair <string, DiscountBase> pair in this.OfferDiscounts) { OfferDiscount offer = pair.Value as OfferDiscount; if (offer != null) { HashSet <decimal> discountLineNumberSetForItem = null; if (offer.ItemGroupIndexToDiscountLineNumberSetMap.TryGetValue(itemGroupIndex, out discountLineNumberSetForItem)) { if (discountLineNumberSetForItem.Count >= 1) { decimal discountLineNumber = discountLineNumberSetForItem.First(); RetailDiscountLine discountLineDefinition = offer.DiscountLines[discountLineNumber]; // For now, it works for fully covered items. // Fully covered, as an example, buy 3 for $10, then if item price is $3, it won't get discount // or buy 3 for $5 off, then if item price is $1, it won't get full discount. bool isFullyCovered = false; simpleDiscountValue = OfferDiscount.GetUnitDiscountAmountAndCheckWhetherItsFullyCovered(discountLineDefinition, price, out isFullyCovered) * quantityNeeded; if (!isFullyCovered) { isOkay = false; } if (!isOkay) { break; } simpleDiscountOfferLookup[itemGroupIndex] = offer; discountOfferCount++; } } } } // For now, we don't handle multiple discount offers. // With optimization earlier, that's mostly the case. See DiscountCalcuator.ReduceOverlappedOfferAndQuantityDiscountsPerItem. if (discountOfferCount > 1) { isOkay = false; } if (!isOkay) { break; } mixAndMatchRelativeValueLookup.Add(itemGroupIndex, mixAndMatchValue - simpleDiscountValue); } } } } return(isOkay); }