private static DiscountBase ConvertDiscountAndLineToDiscountBase(PeriodicDiscount discountAndLine)
            {
                DiscountBase        discount    = null;
                OfferDiscount       offer       = null;
                MixAndMatchDiscount mixAndMatch = null;
                MultipleBuyDiscount multipleBuy = null;
                ThresholdDiscount   threshold   = null;

                switch (discountAndLine.PeriodicDiscountType)
                {
                case PeriodicDiscountOfferType.Offer:
                    offer    = new OfferDiscount(discountAndLine.ValidationPeriod);
                    discount = offer;
                    break;

                case PeriodicDiscountOfferType.MixAndMatch:
                    mixAndMatch = new MixAndMatchDiscount(discountAndLine.ValidationPeriod);
                    mixAndMatch.DealPriceValue              = discountAndLine.MixAndMatchDealPrice;
                    mixAndMatch.DiscountAmountValue         = discountAndLine.MixAndMatchDiscountAmount;
                    mixAndMatch.DiscountPercentValue        = discountAndLine.MixAndMatchDiscountPercent;
                    mixAndMatch.NumberOfLeastExpensiveLines = discountAndLine.MixAndMatchNumberOfLeastExpensiveLines;
                    mixAndMatch.NumberOfTimesApplicable     = discountAndLine.MixAndMatchNumberOfTimeApplicable;
                    mixAndMatch.LeastExpensiveMode          = discountAndLine.LeastExpensiveMode;
                    discount = mixAndMatch;
                    break;

                case PeriodicDiscountOfferType.MultipleBuy:
                    multipleBuy = new MultipleBuyDiscount(discountAndLine.ValidationPeriod);
                    discount    = multipleBuy;
                    break;

                case PeriodicDiscountOfferType.Threshold:
                    threshold = new ThresholdDiscount(discountAndLine.ValidationPeriod);
                    threshold.ShouldCountNonDiscountItems = discountAndLine.ShouldCountNonDiscountItems != 0;
                    discount = threshold;
                    break;
                }

                if (discount != null)
                {
                    discount.IsCategoryToProductOrVariantIdsMapSet = true;

                    discount.OfferId                = discountAndLine.OfferId;
                    discount.OfferName              = discountAndLine.Name;
                    discount.PeriodicDiscountType   = discountAndLine.PeriodicDiscountType;
                    discount.IsDiscountCodeRequired = discountAndLine.IsDiscountCodeRequired;
                    discount.ConcurrencyMode        = discountAndLine.ConcurrencyMode;
                    discount.PricingPriorityNumber  = discountAndLine.PricingPriorityNumber;
                    discount.CurrencyCode           = discountAndLine.CurrencyCode;
                    discount.DateValidationPeriodId = discountAndLine.ValidationPeriodId;
                    discount.DateValidationType     = (DateValidationType)discountAndLine.DateValidationType;
                    discount.DiscountType           = GetDiscountMethodType(discount.PeriodicDiscountType, discountAndLine.DiscountType);
                    discount.ValidFrom              = discountAndLine.ValidFromDate;
                    discount.ValidTo                = discountAndLine.ValidToDate;
                }

                return(discount);
            }
Beispiel #2
0
            /// <summary>
            /// Refreshes item compounded quantity lookup after each round of calculating discounts
            ///     of exclusive or non-exclusive least-expensive-favor-retail discounts, of the same priority number.
            /// Prepares remaining quantities for the next round.
            /// </summary>
            /// <param name="bestDiscountPath">Best discount path of application discount applications so far.</param>
            /// <param name="lastPriorityNumber">Priority number of the last round.</param>
            /// <param name="remainingQuantities">Remaining quantities for the next round.</param>
            /// <param name="remainingQuantitiesForCompound">Remaining quantities for compound for the next round.</param>
            internal void RefreshLookupAndPrepareRemainingQuantitiesForLeastExpensiveFavoRetailer(
                List <AppliedDiscountApplication> bestDiscountPath,
                int lastPriorityNumber,
                decimal[] remainingQuantities,
                decimal[] remainingQuantitiesForCompound)
            {
                // Step 1: fill in this.ItemGroupIndexToCompoundedOfferToQuantityLookup from the last round.
                //         reduce remaining quantitied for non-compounded discounts.
                this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear();
                foreach (AppliedDiscountApplication appliedDiscountApplication in bestDiscountPath)
                {
                    DiscountBase discount = appliedDiscountApplication.DiscountApplication.Discount;

                    if (discount.CanCompound)
                    {
                        // We've already processed the lookups for the previous round. Here we only processed the most recent round.
                        // Only mix and match least expensive favoring retailer
                        MixAndMatchDiscount mixAndMatch = discount as MixAndMatchDiscount;
                        if (discount.PricingPriorityNumber == lastPriorityNumber &&
                            (mixAndMatch != null && mixAndMatch.DiscountType == DiscountMethodType.LeastExpensive && mixAndMatch.LeastExpensiveMode == LeastExpensiveMode.FavorRetailer))
                        {
                            this.FillLookup(appliedDiscountApplication);
                        }
                    }
                    else
                    {
                        ReduceQuantitiesFromNonCompounded(appliedDiscountApplication, remainingQuantities, remainingQuantitiesForCompound);
                    }
                }

                // Step 2: move the data to settled lookups.
                foreach (KeyValuePair <int, Dictionary <string, decimal> > pair in this.ItemGroupIndexToCompoundedOfferToQuantityLookup)
                {
                    int itemGroupIndex = pair.Key;
                    Dictionary <string, decimal> offerIdToQuantityLookup = pair.Value;
                    decimal compoundedQuantity = offerIdToQuantityLookup.Max(p => p.Value);
                    AddToOrUpdateItemGroupIndexToPriorityToSettledCompoundedQuantityLookup(
                        itemGroupIndex,
                        lastPriorityNumber,
                        compoundedQuantity,
                        this.itemGroupIndexToPriorityToSettledCompoundedQuantityForNonThreshold);
                }

                this.ReduceQuantitiesFromCompoundedForNonThresholds(
                    remainingQuantities,
                    remainingQuantitiesForCompound,
                    null);

                // Clear current lookup.
                this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear();
            }
Beispiel #3
0
            /// <summary>
            /// Refreshes item compounded quantity lookup after each round of calculating discounts
            ///     of exclusive or non-exclusive non-thresholds discounts, of the same priority number.
            /// Prepares remaining quantities for the next round.
            /// </summary>
            /// <param name="bestDiscountPath">Best discount path of application discount applications so far.</param>
            /// <param name="lastPriorityNumber">Priority number of the last round.</param>
            /// <param name="remainingQuantities">Remaining quantities for the next round.</param>
            /// <param name="remainingQuantitiesForCompound">Remaining quantities for compound for the next round.</param>
            /// <param name="reserveCompoundedQuantityForLeastExpensiveFavorRetailer">
            /// If it's followed by least expensive favoring retail of the same priority, then reserve the compounded quantity of the same priority.
            /// </param>
            internal void RefreshLookupAndPrepareRemainingQuantitiesForNonThresholds(
                List <AppliedDiscountApplication> bestDiscountPath,
                int lastPriorityNumber,
                decimal[] remainingQuantities,
                decimal[] remainingQuantitiesForCompound,
                bool reserveCompoundedQuantityForLeastExpensiveFavorRetailer)
            {
                // Step 1: fill in this.ItemGroupIndexToCompoundedOfferToQuantityLookup from the last round.
                //         reduce remaining quantitied for non-compounded discounts.
                this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear();
                foreach (AppliedDiscountApplication appliedDiscountApplication in bestDiscountPath)
                {
                    DiscountBase discount = appliedDiscountApplication.DiscountApplication.Discount;

                    if (discount.CanCompound)
                    {
                        // We've already processed the lookups for the previous round. Here we only processed the most recent round.
                        // Ignore mix and match least expensive favoring retailer
                        MixAndMatchDiscount mixAndMatch = discount as MixAndMatchDiscount;
                        if (discount.PricingPriorityNumber == lastPriorityNumber &&
                            discount.PeriodicDiscountType != PeriodicDiscountOfferType.Threshold &&
                            (mixAndMatch == null || mixAndMatch.DiscountType != DiscountMethodType.LeastExpensive || mixAndMatch.LeastExpensiveMode == LeastExpensiveMode.FavorCustomer))
                        {
                            this.FillLookup(appliedDiscountApplication);
                        }
                    }
                    else
                    {
                        ReduceQuantitiesFromNonCompounded(appliedDiscountApplication, remainingQuantities, remainingQuantitiesForCompound);
                    }
                }

                // Step 2: move the data to settled lookups.
                this.MoveToSettledCompoundedQuantityLookup(lastPriorityNumber, this.itemGroupIndexToPriorityToSettledCompoundedQuantityForNonThreshold);

                int?priorityNumberToSkipForCompounded = null;

                if (reserveCompoundedQuantityForLeastExpensiveFavorRetailer)
                {
                    priorityNumberToSkipForCompounded = lastPriorityNumber;
                }

                this.ReduceQuantitiesFromCompoundedForNonThresholds(
                    remainingQuantities,
                    remainingQuantitiesForCompound,
                    priorityNumberToSkipForCompounded);

                // Clear current lookup.
                this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear();
            }
            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);
            }
            internal static Dictionary <long, List <DiscountBase> > GetProductOrVariantToDiscountMapLive(
                SalesTransaction transaction,
                PriceContext priceContext,
                IPricingDataAccessor pricingDataManager)
            {
                List <ItemUnit>           items = new List <ItemUnit>();
                Dictionary <string, long> itemIdInventDimIdToProductOrVariantIdMap = new Dictionary <string, long>(StringComparer.OrdinalIgnoreCase);

                foreach (SalesLine salesLine in transaction.PriceCalculableSalesLines)
                {
                    // The map is to look up product or variant id, but not master id if variant id is present.
                    itemIdInventDimIdToProductOrVariantIdMap[GetItemIdInventDimIdKey(salesLine.ItemId, salesLine.InventoryDimensionId)] = salesLine.ProductId;

                    items.Add(new ItemUnit()
                    {
                        ItemId = salesLine.ItemId, VariantInventoryDimensionId = salesLine.InventoryDimensionId, Product = salesLine.MasterProductId == 0 ? salesLine.ProductId : salesLine.MasterProductId, DistinctProductVariant = salesLine.Variant != null ? salesLine.Variant.DistinctProductVariantId : 0, UnitOfMeasure = Discount.GetUnitOfMeasure(salesLine)
                    });
                }

                ReadOnlyCollection <PeriodicDiscount> discountAndLines = GetRetailDiscountsAndLines(items, priceContext, pricingDataManager, QueryResultSettings.AllRecords);
                ISet <long> productVariantMasterIdsInTransaction       = GetProductVariantMasterIdsForTransaction(transaction);

                Dictionary <long, List <DiscountBase> > productDiscountMap   = new Dictionary <long, List <DiscountBase> >();
                Dictionary <string, DiscountBase>       offerIdToDiscountMap = new Dictionary <string, DiscountBase>(StringComparer.OrdinalIgnoreCase);

                foreach (PeriodicDiscount discountAndLine in discountAndLines)
                {
                    if (!PriceContextHelper.MatchCalculationMode(priceContext, discountAndLine.PeriodicDiscountType))
                    {
                        continue;
                    }

                    string key = GetItemIdInventDimIdKey(discountAndLine.ItemId, discountAndLine.InventoryDimensionId);
                    long   productOrVariantId = 0;
                    if (itemIdInventDimIdToProductOrVariantIdMap.TryGetValue(key, out productOrVariantId))
                    {
                        DiscountBase discount = null;

                        if (offerIdToDiscountMap.TryGetValue(discountAndLine.OfferId, out discount))
                        {
                            RetailDiscountLine discountLine = null;
                            if (!discount.DiscountLines.TryGetValue(discountAndLine.DiscountLineNumber, out discountLine))
                            {
                                discountLine = ConvertDiscountAndLineToDiscountLine(discountAndLine, discount);
                                discount.DiscountLines.Add(discountLine.DiscountLineNumber, discountLine);
                            }

                            IList <RetailDiscountLine> discountLines = null;
                            if (discount.ProductOfVariantToDiscountLinesMap.TryGetValue(productOrVariantId, out discountLines))
                            {
                                discountLines.Add(discountLine);
                            }
                            else
                            {
                                discount.ProductOfVariantToDiscountLinesMap[productOrVariantId] = new List <RetailDiscountLine> {
                                    discountLine
                                };
                            }
                        }
                        else
                        {
                            discount = ConvertDiscountAndLineToDiscountBase(discountAndLine);
                            discount.ProductOrVariantIdsInTransaction = productVariantMasterIdsInTransaction;
                            RetailDiscountLine discountLine = ConvertDiscountAndLineToDiscountLine(discountAndLine, discount);
                            discount.DiscountLines.Add(discountLine.DiscountLineNumber, discountLine);
                            offerIdToDiscountMap.Add(discount.OfferId, discount);
                            discount.ProductOfVariantToDiscountLinesMap[productOrVariantId] = new List <RetailDiscountLine> {
                                discountLine
                            };
                        }

                        List <DiscountBase> discounts;
                        if (productDiscountMap.TryGetValue(productOrVariantId, out discounts))
                        {
                            if (!discounts.Where(p => p.OfferId == discount.OfferId).Any())
                            {
                                discounts.Add(discount);
                            }
                        }
                        else
                        {
                            productDiscountMap[productOrVariantId] = new List <DiscountBase>()
                            {
                                discount
                            };
                        }
                    }
                }

                IEnumerable <string> offerIds = offerIdToDiscountMap.Select(p => p.Key);

                if (offerIds.Any())
                {
                    IEnumerable <DiscountCode> discountCodes = pricingDataManager.GetDiscountCodesByOfferIds(offerIds) as IEnumerable <DiscountCode>;

                    foreach (DiscountCode discountCode in discountCodes)
                    {
                        DiscountBase discountBase;
                        if (offerIdToDiscountMap.TryGetValue(discountCode.OfferId, out discountBase))
                        {
                            // Accept both discount code and barcode in retail channel.
                            discountBase.DiscountCodes.Add(discountCode.Code);
                            discountBase.DiscountCodes.Add(discountCode.Barcode);
                        }
                    }

                    IEnumerable <RetailDiscountPriceGroup> discountPriceGroups = pricingDataManager.GetRetailDiscountPriceGroups(new HashSet <string>(offerIds)) as IEnumerable <RetailDiscountPriceGroup>;

                    foreach (RetailDiscountPriceGroup discountPriceGroup in discountPriceGroups)
                    {
                        offerIdToDiscountMap[discountPriceGroup.OfferId].PriceDiscountGroupIds.Add(discountPriceGroup.PriceGroupId);
                    }

                    SetEffectiveDiscountPriorityFromPriceGroups(offerIdToDiscountMap, priceContext);

                    IEnumerable <string> quantityOfferIds = offerIdToDiscountMap.Where(p => p.Value.PeriodicDiscountType == PeriodicDiscountOfferType.MultipleBuy).Select(p => p.Key);

                    if (quantityOfferIds.Any())
                    {
                        IEnumerable <QuantityDiscountLevel> quantityLevels = pricingDataManager.GetMultipleBuyDiscountLinesByOfferIds(quantityOfferIds) as IEnumerable <QuantityDiscountLevel>;

                        foreach (QuantityDiscountLevel quantityLevel in quantityLevels)
                        {
                            DiscountBase discountBase;
                            if (offerIdToDiscountMap.TryGetValue(quantityLevel.OfferId, out discountBase))
                            {
                                MultipleBuyDiscount multipleBuy = discountBase as MultipleBuyDiscount;

                                if (multipleBuy != null)
                                {
                                    multipleBuy.QuantityDiscountLevels.Add(quantityLevel);
                                }
                            }
                        }
                    }

                    IEnumerable <string> mixMatchOfferIds = offerIdToDiscountMap.Where(p => p.Value.PeriodicDiscountType == PeriodicDiscountOfferType.MixAndMatch).Select(p => p.Key);

                    if (mixMatchOfferIds.Any())
                    {
                        IEnumerable <MixAndMatchLineGroup> mixMatchLineGroups = pricingDataManager.GetMixAndMatchLineGroupsByOfferIds(mixMatchOfferIds) as IEnumerable <MixAndMatchLineGroup>;

                        foreach (MixAndMatchLineGroup lineGroup in mixMatchLineGroups)
                        {
                            DiscountBase discountBase;
                            if (offerIdToDiscountMap.TryGetValue(lineGroup.OfferId, out discountBase))
                            {
                                MixAndMatchDiscount mixMatch = discountBase as MixAndMatchDiscount;

                                if (mixMatch != null)
                                {
                                    mixMatch.LineGroupToNumberOfItemsMap.Add(lineGroup.LineGroup, lineGroup.NumberOfItemsNeeded);
                                }
                            }
                        }
                    }

                    IEnumerable <string> thresholdOfferIds = offerIdToDiscountMap.Where(p => p.Value.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold).Select(p => p.Key);

                    if (thresholdOfferIds.Any())
                    {
                        IEnumerable <ThresholdDiscountTier> thresholdTiers = pricingDataManager.GetThresholdTiersByOfferIds(thresholdOfferIds) as IEnumerable <ThresholdDiscountTier>;

                        foreach (ThresholdDiscountTier tier in thresholdTiers)
                        {
                            DiscountBase discountBase;
                            if (offerIdToDiscountMap.TryGetValue(tier.OfferId, out discountBase))
                            {
                                ThresholdDiscount threshold = discountBase as ThresholdDiscount;

                                if (threshold != null)
                                {
                                    threshold.ThresholdDiscountTiers.Add(tier);
                                }
                            }
                        }
                    }
                }

                return(productDiscountMap);
            }
            /// <summary>
            /// Converts retail discount data from database to discount object.
            /// </summary>
            /// <param name="retailDiscount">Retail discount data from database.</param>
            /// <returns>Discount object.</returns>
            /// <remarks>This is private. Exposed as internal for test.</remarks>
            internal static DiscountBase ConvertRetailDiscountToDiscountBase(RetailDiscount retailDiscount)
            {
                DiscountBase        discount    = null;
                OfferDiscount       offer       = null;
                MixAndMatchDiscount mixAndMatch = null;
                MultipleBuyDiscount multipleBuy = null;
                ThresholdDiscount   threshold   = null;

                switch (retailDiscount.PeriodicDiscountType)
                {
                case PeriodicDiscountOfferType.Offer:
                    offer    = new OfferDiscount(retailDiscount.ValidationPeriod);
                    discount = offer;
                    break;

                case PeriodicDiscountOfferType.MixAndMatch:
                    mixAndMatch = new MixAndMatchDiscount(retailDiscount.ValidationPeriod);
                    mixAndMatch.DealPriceValue              = retailDiscount.MixAndMatchDealPrice;
                    mixAndMatch.DiscountAmountValue         = retailDiscount.MixAndMatchDiscountAmount;
                    mixAndMatch.DiscountPercentValue        = retailDiscount.MixAndMatchDiscountPercent;
                    mixAndMatch.NumberOfLeastExpensiveLines = retailDiscount.MixAndMatchNumberOfLeastExpensiveLines;
                    mixAndMatch.LeastExpensiveMode          = retailDiscount.LeastExpensiveMode;
                    mixAndMatch.NumberOfTimesApplicable     = retailDiscount.MixAndMatchNumberOfTimeApplicable;
                    foreach (RetailDiscountLine mixMatchLine in retailDiscount.DiscountLines)
                    {
                        if (!mixAndMatch.LineGroupToNumberOfItemsMap.ContainsKey(mixMatchLine.MixAndMatchLineGroup))
                        {
                            mixAndMatch.LineGroupToNumberOfItemsMap.Add(mixMatchLine.MixAndMatchLineGroup, mixMatchLine.MixAndMatchLineNumberOfItemsNeeded);
                        }
                    }

                    discount = mixAndMatch;
                    break;

                case PeriodicDiscountOfferType.MultipleBuy:
                    multipleBuy = new MultipleBuyDiscount(retailDiscount.ValidationPeriod);
                    multipleBuy.QuantityDiscountLevels.AddRange(retailDiscount.MultibuyQuantityTiers);
                    discount = multipleBuy;
                    break;

                case PeriodicDiscountOfferType.Threshold:
                    threshold = new ThresholdDiscount(retailDiscount.ValidationPeriod);
                    threshold.ShouldCountNonDiscountItems = retailDiscount.ShouldCountNonDiscountItems != 0;
                    threshold.ThresholdDiscountTiers.AddRange(retailDiscount.ThresholdDiscountTiers);
                    discount = threshold;
                    break;
                }

                if (discount != null)
                {
                    discount.IsCategoryToProductOrVariantIdsMapSet = false;

                    discount.OfferId                = retailDiscount.OfferId;
                    discount.OfferName              = retailDiscount.Name;
                    discount.PeriodicDiscountType   = retailDiscount.PeriodicDiscountType;
                    discount.IsDiscountCodeRequired = retailDiscount.IsDiscountCodeRequired;
                    discount.ConcurrencyMode        = retailDiscount.ConcurrencyMode;
                    discount.PricingPriorityNumber  = retailDiscount.PricingPriorityNumber;
                    discount.CurrencyCode           = retailDiscount.CurrencyCode;
                    discount.DateValidationPeriodId = retailDiscount.ValidationPeriodId;
                    discount.DateValidationType     = (DateValidationType)retailDiscount.DateValidationType;
                    discount.DiscountType           = GetDiscountMethodType(discount.PeriodicDiscountType, retailDiscount.DiscountType);
                    discount.ValidFrom              = retailDiscount.ValidFromDate;
                    discount.ValidTo                = retailDiscount.ValidToDate;

                    foreach (RetailDiscountLine discountLine in retailDiscount.DiscountLines)
                    {
                        discountLine.DiscountMethod = (int)GetLineDiscountOfferMethod(discount.PeriodicDiscountType, discount.DiscountType, discountLine.DiscountMethod, discountLine.MixAndMatchLineSpecificDiscountType);
                        discount.DiscountLines.Add(discountLine.DiscountLineNumber, discountLine);
                    }

                    foreach (RetailDiscountPriceGroup priceGroup in retailDiscount.PriceGroups)
                    {
                        discount.PriceDiscountGroupIds.Add(priceGroup.PriceGroupId);
                    }

                    foreach (DiscountCode discountCode in retailDiscount.DiscountCodes)
                    {
                        discount.DiscountCodes.Add(discountCode.Code);
                        discount.DiscountCodes.Add(discountCode.Barcode);
                    }
                }

                return(discount);
            }