Example #1
0
            /// <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);
            }
Example #2
0
            /// <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);
                }
            }
Example #3
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);
            }
Example #4
0
            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));
                                    }
                                }
                            }
                        }
                    }
                }
            }