Example #1
0
            internal static decimal GetUnitDiscountAmountAndCheckWhetherItsFullyCovered(RetailDiscountLine discountLineDefinition, decimal price, out bool isFullyCovered)
            {
                isFullyCovered = true;
                decimal unitDiscountAmount = decimal.Zero;

                switch ((DiscountOfferMethod)discountLineDefinition.DiscountMethod)
                {
                case DiscountOfferMethod.DiscountAmount:
                    unitDiscountAmount = discountLineDefinition.DiscountAmount;
                    isFullyCovered     = unitDiscountAmount <= price;
                    break;

                case DiscountOfferMethod.DiscountPercent:
                    unitDiscountAmount = DiscountBase.GetDiscountAmountForPercentageOff(price, discountLineDefinition.DiscountPercent);
                    break;

                case DiscountOfferMethod.OfferPrice:
                    unitDiscountAmount = DiscountBase.GetDiscountAmountForDealUnitPrice(price, discountLineDefinition.OfferPrice);
                    isFullyCovered     = discountLineDefinition.OfferPrice <= price;
                    break;

                default:
                    break;
                }

                unitDiscountAmount = Math.Min(price, unitDiscountAmount);
                unitDiscountAmount = Math.Max(unitDiscountAmount, decimal.Zero);

                return(unitDiscountAmount);
            }
            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);
            }