Example #1
0
        /// <summary>
        /// 查询客户产品折扣表
        /// </summary>
        /// <param name="DiscountDTOList"></param>
        /// <returns></returns>
        public List <ReturnDiscountDTO> GetDiscount(List <InDiscountDTO> DiscountDTOList)
        {
            List <ReturnDiscountDTO> ReturnDiscountList = new List <ReturnDiscountDTO>();

            //查找条件:
            //客户产品折扣表.客户=销售订单.客户
            //客户产品折扣表.产品=销售订单行.料品
            //客户产品折扣表行.币种=销售订单.币种
            for (int i = 0; i < DiscountDTOList.Count; i++)
            {
                //DiscountHead.EntityList DiscountList = DiscountHead.Finder.FindAll("Custmor.ID={0} and DiscountLine.CurrencyType.ID={1} and Product.ID={2}", new OqlParam[] { new OqlParam(DiscountDTOList[i].Customer.ID), new OqlParam(DiscountDTOList[i].Currency.ID), new OqlParam(DiscountDTOList[i].ItemMaster.ID) });

                DiscountLine Discount = DiscountLine.Finder.Find(string.Format("DiscountHead.Custmor.ID={0}  and DiscountHead.Product.ID={1} and ValidDate<=@ValidDate and UnValidDate>=@UnValidDate", DiscountDTOList[i].Customer.ID, DiscountDTOList[i].ItemMaster.ID), new OqlParam(DateTime.Now.ToString()), new OqlParam(DateTime.Now.ToString()));
                //and CurrencyType.ID={1}  DiscountDTOList[i].Currency.ID,
                // 折扣表单价。折扣方式,折扣比例,外销价,销售订单行ID
                if (Discount != null)
                {
                    ReturnDiscountDTO ReturnDiscount = new ReturnDiscountDTO();
                    ReturnDiscount.DiscountID     = Discount.ID;                     //折扣表行ID
                    ReturnDiscount.DiscountPrices = Discount.Prices;                 //单价
                    ReturnDiscount.DiscountRatio  = Discount.Discount;               //折扣比例
                    ReturnDiscount.DiscountType   = Discount.DiscountType;           //折扣方式
                    ReturnDiscount.SoLineID       = DiscountDTOList[i].SoLineID;     //销售订单行ID
                    ReturnDiscount.ExportPrices   = DiscountDTOList[i].ExportPrices; //外销价
                    ReturnDiscount.Currency       = DiscountDTOList[i].Currency;     //币种
                    ReturnDiscountList.Add(ReturnDiscount);
                }
            }
            return(ReturnDiscountList);
        }
Example #2
0
            private static bool CanApplyCompoundedDiscount(
                IEnumerable <DiscountLine> existingDiscountLines,
                DiscountLine newDiscountLine,
                bool requiresExistingCompounded)
            {
                bool canApplyCompoundedDiscount = true;

                if (newDiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold ||
                    newDiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Offer)
                {
                    // Threshold and discount offer don't discount for partial quantity, so no need to worry about requiresExistingCompounded;
                    canApplyCompoundedDiscount = CanCompoundTogether(existingDiscountLines, newDiscountLine);
                }
                else
                {
                    // This is really for multiple compounded mix and match discounts, each of which taking partial quantity.
                    // We'd compound them together as much as possible.
                    if (existingDiscountLines.Any())
                    {
                        canApplyCompoundedDiscount = CanCompoundTogether(existingDiscountLines, newDiscountLine);
                    }
                    else
                    {
                        // No existing discount
                        if (requiresExistingCompounded)
                        {
                            canApplyCompoundedDiscount = false;
                        }
                    }
                }

                return(canApplyCompoundedDiscount);
            }
Example #3
0
        public void createDiscountLine()
        {
            getApiKey();
            conekta.Api.version = "2.0.0";

            Order order = new conekta.Order().create(@"{
                  ""currency"":""MXN"",
                  ""customer_info"": {
                  ""name"": ""Jul Ceballos"",
                  ""phone"": ""+5215555555555"",
                  ""email"": ""*****@*****.**""
                  },
                  ""line_items"": [{
                  ""name"": ""Box of Cohiba S1s"",
                  ""unit_price"": 35000,
                  ""quantity"": 1
                  }]
                  }");

            Assert.AreEqual(order.id.GetType().ToString(), "System.String");

            order = new Order().find(order.id);

            DiscountLine discount_line = order.createDiscountLine(@"{
                ""code"": ""123"",
                ""type"": ""loyalty"",
                ""amount"": 600
                }");

            Assert.AreEqual(discount_line.code, "123");
            Assert.AreEqual(discount_line.type, "loyalty");
        }
Example #4
0
            /// <summary>
            /// Update the discount items.
            /// </summary>
            /// <param name="saleItem">The item line that the discount line is added to.</param>
            /// <param name="discountItem">The new discount line to add.</param>
            internal static void UpdateDiscountLines(SalesLine saleItem, DiscountLine discountItem)
            {
                // Check if line discount is found, if so then update
                bool discountLineFound = false;

                foreach (var discLine in saleItem.DiscountLines)
                {
                    if (discLine.DiscountLineType == DiscountLineType.CustomerDiscount)
                    {
                        // If found then update
                        if ((discLine.DiscountLineType == discountItem.DiscountLineType) &&
                            (discLine.CustomerDiscountType == discountItem.CustomerDiscountType))
                        {
                            discLine.Percentage = discountItem.Percentage;
                            discLine.Amount     = discountItem.Amount;
                            discountLineFound   = true;
                        }
                    }
                }

                // If line discount is not found then add it.
                if (!discountLineFound)
                {
                    saleItem.DiscountLines.Add(discountItem);
                }

                if (discountItem.DiscountLineType == DiscountLineType.CustomerDiscount)
                {
                    saleItem.ResetLineMultilineDiscountOnItem();
                }

                saleItem.WasChanged = true;
            }
Example #5
0
            /// <summary>
            /// Retrieves a customer discount item of the indicated type if it exists and creates one if not.
            /// </summary>
            /// <param name="salesLine">The sales line from which to find customer discount lines.</param>
            /// <param name="customerDiscountType">The customer discount type.</param>
            /// <param name="lineDiscountType">The line discount type.</param>
            /// <returns>
            /// The discount line.
            /// </returns>
            private static DiscountLine GetCustomerDiscountItem(SalesLine salesLine, CustomerDiscountType customerDiscountType, DiscountLineType lineDiscountType)
            {
                DiscountLine discount;
                var          discounts = from d in salesLine.DiscountLines
                                         where
                                         d.DiscountLineType == lineDiscountType &&
                                         d.CustomerDiscountType == customerDiscountType
                                         select d;

                // If the discount doesn't exist create a new one
                if (discounts.Count() == 0)
                {
                    discount = new DiscountLine
                    {
                        DiscountLineType     = lineDiscountType,
                        CustomerDiscountType = customerDiscountType,
                    };

                    salesLine.DiscountLines.Add(discount);
                }
                else
                {
                    // otherwise select it.
                    discount = discounts.First();
                }

                return(discount);
            }
            /// <summary>
            /// Updates the sales transaction with the quantity promotion if applicable.
            /// </summary>
            /// <param name="existingTransaction">Existing transaction.</param>
            /// <param name="tempSalesTransaction">Copy of existing transaction.</param>
            /// <param name="context">The request context.</param>
            /// <param name="salesLineIndex">The sales line under consideration.</param>
            /// <param name="discountLine">The quantity discount under consideration.</param>
            /// <param name="multiBuyDiscountLines">The multi buy discount lines.</param>
            private static void GetQuantityPromotions(SalesTransaction existingTransaction, SalesTransaction tempSalesTransaction, RequestContext context, int salesLineIndex, DiscountLine discountLine, IEnumerable <QuantityDiscountLevel> multiBuyDiscountLines)
            {
                // Get the multi buy discount lines for the current multi buy discount.
                IEnumerable <QuantityDiscountLevel> multiBuyLinesForCurrentOffer = multiBuyDiscountLines.Where(j => j.OfferId.Equals(discountLine.OfferId)).OrderBy(l => l.MinimumQuantity);

                List <SalesLine> salesLinesWithSameProduct = tempSalesTransaction.SalesLines.Where(j => j.ItemId == tempSalesTransaction.SalesLines[salesLineIndex].ItemId && j.InventoryDimensionId == tempSalesTransaction.SalesLines[salesLineIndex].InventoryDimensionId).ToList();
                decimal          totalQuantity             = salesLinesWithSameProduct.Select(j => j.Quantity).Sum();
                decimal          currentQuantity           = tempSalesTransaction.SalesLines[salesLineIndex].Quantity;

                salesLinesWithSameProduct.Remove(tempSalesTransaction.SalesLines[salesLineIndex]);
                bool neverApplied = true;

                foreach (QuantityDiscountLevel multiBuyLine in multiBuyLinesForCurrentOffer)
                {
                    // removing the quantity discounts that were not applied (because of concurrency rules).
                    if (multiBuyLine.MinimumQuantity <= totalQuantity)
                    {
                        continue;
                    }

                    // Temporarily update the current transaction with the new quantity to see if the quantity discount will be applied.
                    existingTransaction.SalesLines[salesLineIndex].Quantity = multiBuyLine.MinimumQuantity - totalQuantity + currentQuantity;

                    CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All);
                    DiscountLine isApplied = existingTransaction.SalesLines[salesLineIndex].DiscountLines.Where(j => j.OfferId == discountLine.OfferId).SingleOrDefault();

                    // If the quantity discount will be applied then remove the discount line from the lines with same product and get the min quantity to buy for discount.
                    if (isApplied != null && (isApplied.Amount != 0 || isApplied.Percentage != 0))
                    {
                        int toBuy = (int)(multiBuyLine.MinimumQuantity - totalQuantity);
                        if (isApplied.Amount != 0)
                        {
                            discountLine.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.MultiBuyDiscountPricePromotion, toBuy, Math.Round(isApplied.Amount, 2));
                        }
                        else
                        {
                            discountLine.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.MultiBuyDiscountPercentagePromotion, toBuy, Math.Round(isApplied.Percentage, 2));
                        }

                        neverApplied = false;
                        break;
                    }
                }

                if (neverApplied)
                {
                    tempSalesTransaction.SalesLines[salesLineIndex].DiscountLines.Remove(discountLine);
                }

                existingTransaction.SalesLines[salesLineIndex].Quantity = currentQuantity;
                CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All);

                foreach (SalesLine sameproductCartLine in salesLinesWithSameProduct)
                {
                    sameproductCartLine.DiscountLines.Remove(sameproductCartLine.DiscountLines.Where(k => k.OfferId == discountLine.OfferId).SingleOrDefault());
                }
            }
Example #7
0
            /// <summary>
            /// Attempts to apply a DiscountLine to a SalesLine that has a larger quantity than the required quantity, splitting the line if it is found.
            /// </summary>
            /// <param name="transaction">The current transaction containing the lines.</param>
            /// <param name="availableLines">The available line indices on the transaction.</param>
            /// <param name="quantityNeeded">The quantity needed for the DiscountLine.</param>
            /// <param name="discount">The DiscountLine and original quantity needed.</param>
            /// <param name="isReturn">True if it's return.</param>
            /// <param name="requiresExistingCompoundedDiscounts">A flag indicating whether it requires existing compounded discounts on the line to compound on top of.</param>
            /// <returns>True if a match was found and the discount was applied, false otherwise.</returns>
            private bool ApplyDiscountLineForLargerMatch(
                SalesTransaction transaction,
                HashSet <int> availableLines,
                ref decimal quantityNeeded,
                DiscountLineQuantity discount,
                bool isReturn,
                bool requiresExistingCompoundedDiscounts)
            {
                bool discountApplied = false;

                foreach (int x in availableLines.ToList().OrderBy(p => this.salesLines[p].Quantity))
                {
                    SalesLine salesLine = this.salesLines[x];

                    if (!IsQuantityMatchSalesOrReturn(isReturn, salesLine.Quantity))
                    {
                        continue;
                    }

                    decimal lineQuantity = Math.Abs(salesLine.Quantity);
                    if (lineQuantity > quantityNeeded)
                    {
                        if (discount.DiscountLine.ConcurrencyMode != ConcurrencyMode.Compounded ||
                            CanApplyCompoundedDiscount(this.salesLines[x].DiscountLines, discount.DiscountLine, requiresExistingCompoundedDiscounts))
                        {
                            // Perform the split of this line
                            SalesLine newLine = this.SplitLine(this.salesLines[x], quantityNeeded);

                            // 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);

                            DiscountLine discountLine = discount.DiscountLine.Clone <DiscountLine>();
                            discountLine.SaleLineNumber = newLine.LineNumber;
                            newLine.DiscountLines.Add(discountLine);
                            newLine.QuantityDiscounted = quantityNeeded * Math.Sign(salesLine.Quantity);

                            // If this is a compounding discount, add the new line to the available lines.
                            if (discount.DiscountLine.ConcurrencyMode == ConcurrencyMode.Compounded)
                            {
                                availableLines.Add(this.salesLines.Count - 1);
                            }

                            discountApplied = true;
                            quantityNeeded  = 0;
                            break;
                        }
                    }
                }

                return(discountApplied);
            }
Example #8
0
            /// <summary>
            /// Calculate the periodic discounts for the transaction.
            /// </summary>
            /// <param name="pricingDataManager">Provides data access to the calculation.</param>
            /// <param name="currencyAndRoundingHelper">Currency and rounding helper.</param>
            /// <param name="transaction">The sales transaction.</param>
            /// <param name="customer">Customer for the transaction containing customer discount groups.</param>
            /// <param name="doesPriceIncludeTax">Does the channel have tax-inclusive prices.</param>
            /// <param name="currencyCode">Currency code to filter discounts by.</param>
            /// <param name="channelDateTime">Channel time.</param>
            internal static void GetAllPeriodicDisc(
                IPricingDataAccessor pricingDataManager,
                ICurrencyOperations currencyAndRoundingHelper,
                SalesTransaction transaction,
                Customer customer,
                bool doesPriceIncludeTax,
                string currencyCode,
                DateTimeOffset channelDateTime)
            {
                if (transaction == null)
                {
                    throw new ArgumentNullException("transaction");
                }

                // Clear all periodic, customer, standard, total discounts
                ClearDiscountLinesOfType(transaction, null);

                PriceContext priceContext = BuildPriceContext(pricingDataManager, currencyAndRoundingHelper, customer, transaction, currencyCode, channelDateTime, doesPriceIncludeTax, DiscountCalculationMode.CalculateAll);

                PricingEngine.PopulateProductIds(pricingDataManager, priceContext, transaction);

                ReadOnlyCollection <PeriodicDiscount> periodicDiscountData = GetRetailDiscounts(transaction.ActiveSalesLines, priceContext, pricingDataManager, QueryResultSettings.AllRecords);

                foreach (SalesLine salesLine in transaction.PriceCalculableSalesLines)
                {
                    ReadOnlyCollection <PeriodicDiscount> applicablePeriodicDiscounts = Discount.GetPeriodicDiscountData(periodicDiscountData, salesLine.ItemId, salesLine.InventoryDimensionId);

                    foreach (PeriodicDiscount periodicDiscount in applicablePeriodicDiscounts)
                    {
                        var discountLine = new DiscountLine
                        {
                            OfferId                   = periodicDiscount.OfferId,
                            OfferName                 = periodicDiscount.Name,
                            Percentage                = periodicDiscount.DiscountPercent,
                            Amount                    = periodicDiscount.DiscountAmount,
                            ConcurrencyMode           = periodicDiscount.ConcurrencyMode,
                            ConcurrencyModeValue      = (int)periodicDiscount.ConcurrencyMode,
                            DiscountLineType          = DiscountLineType.PeriodicDiscount,
                            DiscountLineTypeValue     = (int)DiscountLineType.PeriodicDiscount,
                            PeriodicDiscountType      = periodicDiscount.PeriodicDiscountType,
                            PeriodicDiscountTypeValue = (int)periodicDiscount.PeriodicDiscountType,
                            DiscountApplicationGroup  = salesLine.ItemId,
                            IsDiscountCodeRequired    = periodicDiscount.IsDiscountCodeRequired
                        };

                        salesLine.DiscountLines.Add(discountLine);
                    }
                }
            }
            /// <summary>
            /// Apply the given multiline discount row to the given sales line if discount amounts have been specified.
            /// </summary>
            /// <param name="salesLine">The sales line which will receive the discount.</param>
            /// <param name="percent1">Percentage one.</param>
            /// <param name="percent2">Percentage two.</param>
            /// <param name="discountAmount">Discount amount.</param>
            private static void ApplyMultilineDiscount(SalesLine salesLine, decimal percent1, decimal percent2, decimal discountAmount)
            {
                if (percent1 > decimal.Zero || percent2 > decimal.Zero || discountAmount > decimal.Zero)
                {
                    DiscountLine discountItem = new DiscountLine
                    {
                        DiscountLineType     = DiscountLineType.CustomerDiscount,
                        CustomerDiscountType = CustomerDiscountType.MultilineDiscount,
                        Percentage           = DiscountLine.GetCompoundedPercentage(percent1, percent2),
                        Amount = discountAmount,
                    };

                    Discount.UpdateDiscountLines(salesLine, discountItem);
                }
            }
Example #10
0
            private static DiscountLine NewDiscountLineCompoundedThreshold(string offerId, decimal salesLineNumber, decimal amount, int priorityNumber)
            {
                DiscountLine discountLine = new DiscountLine()
                {
                    OfferId               = offerId,
                    SaleLineNumber        = salesLineNumber,
                    DiscountLineType      = Microsoft.Dynamics.Commerce.Runtime.DataModel.DiscountLineType.PeriodicDiscount,
                    PeriodicDiscountType  = PeriodicDiscountOfferType.Threshold,
                    ConcurrencyMode       = ConcurrencyMode.Compounded,
                    IsCompoundable        = true,
                    PricingPriorityNumber = priorityNumber,
                    Amount = amount,
                };

                return(discountLine);
            }
Example #11
0
            private static void ParseAndCreateDiscountLine(XElement discountDetail, SalesLine lineItem)
            {
                DiscountLine lineDiscount = new DiscountLine();

                lineDiscount.EffectiveAmount = Convert.ToDecimal(discountDetail.Attribute("Amount").Value);
                lineDiscount.Amount          = Convert.ToDecimal(discountDetail.Attribute("DiscountAmount").Value);

                lineDiscount.DiscountLineType     = (DiscountLineType)Convert.ToInt32(discountDetail.Attribute("DiscountOriginType").Value);
                lineDiscount.DiscountCode         = discountDetail.Attribute("DiscountCode").Value;
                lineDiscount.CustomerDiscountType = (CustomerDiscountType)Convert.ToInt32(discountDetail.Attribute("CustomerDiscountType").Value);
                lineDiscount.ManualDiscountType   = (ManualDiscountType)Convert.ToInt32(discountDetail.Attribute("ManualDiscountType").Value);
                lineDiscount.OfferId    = discountDetail.Attribute("PeriodicDiscountOfferId").Value;
                lineDiscount.Percentage = Convert.ToDecimal(discountDetail.Attribute("Percentage").Value);
                lineDiscount.DealPrice  = Convert.ToDecimal(discountDetail.Attribute("DealPrice").Value);

                lineItem.DiscountLines.Add(lineDiscount);
            }
Example #12
0
            /// <summary>
            /// Attempts to apply a DiscountLine to a SalesLine that has a smaller quantity than the required quantity.
            /// </summary>
            /// <param name="availableLines">The available line indices on the transaction.</param>
            /// <param name="quantityNeeded">The quantity needed for the DiscountLine.</param>
            /// <param name="discount">The DiscountLine and original quantity needed.</param>
            /// <param name="isReturn">True if it's return.</param>
            /// <param name="requiresExistingCompoundedDiscounts">A flag indicating whether it requires existing compounded discounts on the line to compound on top of.</param>
            /// <returns>True if the discount was applied to a line, false otherwise.</returns>
            private bool ApplyDiscountLineForSmallerMatch(
                HashSet <int> availableLines,
                ref decimal quantityNeeded,
                DiscountLineQuantity discount,
                bool isReturn,
                bool requiresExistingCompoundedDiscounts)
            {
                bool discountPartiallyApplied = false;

                foreach (int x in availableLines.ToList().OrderByDescending(p => this.salesLines[p].Quantity))
                {
                    SalesLine salesLine = this.salesLines[x];

                    if (!IsQuantityMatchSalesOrReturn(isReturn, salesLine.Quantity))
                    {
                        continue;
                    }

                    decimal lineQuantity = Math.Abs(salesLine.Quantity);
                    if (lineQuantity < quantityNeeded)
                    {
                        if (discount.DiscountLine.ConcurrencyMode != ConcurrencyMode.Compounded ||
                            CanApplyCompoundedDiscount(this.salesLines[x].DiscountLines, discount.DiscountLine, requiresExistingCompoundedDiscounts))
                        {
                            DiscountLine discountLine = discount.DiscountLine.Clone <DiscountLine>();
                            discountLine.SaleLineNumber = salesLine.LineNumber;
                            salesLine.DiscountLines.Add(discountLine);
                            salesLine.QuantityDiscounted = salesLine.Quantity;

                            if (discount.DiscountLine.ConcurrencyMode != ConcurrencyMode.Compounded)
                            {
                                availableLines.Remove(x);
                            }

                            discountPartiallyApplied = true;
                            quantityNeeded          -= lineQuantity;
                            break;
                        }
                    }
                }

                return(discountPartiallyApplied);
            }
Example #13
0
        public void updateDiscountLine()
        {
            conekta.Api.apiKey  = "key_eYvWV7gSDkNYXsmr";
            conekta.Api.version = "2.0.0";

            Order order = new conekta.Order().create(@"{
	            ""currency"":""MXN"",
				""customer_info"": {
					""name"": ""Jul Ceballos"",
					""phone"": ""5575553324"",
					""email"": ""*****@*****.**""
				},
	            ""line_items"": [{
				   ""name"": ""Box of Cohiba S1s"",
				   ""unit_price"": 35000,
				   ""quantity"": 1
				}]
	        }"    );

            Assert.AreEqual(order.id.GetType().ToString(), "System.String");

            order = new Order().find(order.id);

            DiscountLine discount_line = order.createDiscountLine(@"{
			    ""code"": ""234"",
			    ""type"": ""loyalty"",
			    ""amount"": 600
			}"            );

            order = new Order().find(order.id);

            discount_line = (DiscountLine)order.discount_lines.at(0);
            discount_line = discount_line.update(@"{
			    ""amount"": 700,
				""code"": ""567"",
			    ""type"": ""coupon""
			}"            );

            Assert.AreEqual(discount_line.type, "coupon");
            Assert.AreEqual(discount_line.code, "567");
            Assert.AreEqual(discount_line.amount, 700);
        }
Example #14
0
            /// <summary>
            /// Calculates manual line discount sent from cashier.
            /// Should be called only after other discounts are calculated.
            /// </summary>
            /// <param name="transaction">Transaction to calculate manual total discounts on.</param>
            public void CalculateLineManualDiscount(SalesTransaction transaction)
            {
                if (transaction == null)
                {
                    throw new ArgumentNullException("transaction");
                }

                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                foreach (var salesLine in transaction.PriceCalculableSalesLines)
                {
                    Discount.ClearManualDiscountLinesOfType(salesLine, ManualDiscountType.LineDiscountAmount);
                    Discount.ClearManualDiscountLinesOfType(salesLine, ManualDiscountType.LineDiscountPercent);

                    DiscountLine lineDiscountItem = null;

                    if (salesLine.LineManualDiscountPercentage != 0)
                    {
                        // Add a new line discount
                        lineDiscountItem = new DiscountLine
                        {
                            DiscountLineType   = DiscountLineType.ManualDiscount,
                            ManualDiscountType = ManualDiscountType.LineDiscountPercent,
                            Percentage         = salesLine.LineManualDiscountPercentage,
                        };

                        this.AddLineDiscount(transaction, salesLine, lineDiscountItem);
                    }

                    if (salesLine.LineManualDiscountAmount != 0)
                    {
                        // Add a new line discount
                        lineDiscountItem = new DiscountLine
                        {
                            DiscountLineType   = DiscountLineType.ManualDiscount,
                            ManualDiscountType = ManualDiscountType.LineDiscountAmount,
                            Amount             = salesLine.Quantity != decimal.Zero ? salesLine.LineManualDiscountAmount / salesLine.Quantity : decimal.Zero,
                        };

                        this.AddLineDiscount(transaction, salesLine, lineDiscountItem);
                    }
                }
            }
Example #15
0
 /// <summary>
 /// Adds total discount lines to the item lines.
 /// </summary>
 /// <param name="transaction">The transaction receiving total discount lines.</param>
 private void AddTotalDiscPctLines(SalesTransaction transaction)
 {
     // Consider calculable lines only. Ignore voided or return-by-receipt lines.
     // Add the total discount to each item.
     foreach (var saleItem in transaction.PriceCalculableSalesLines)
     {
         if (PriceContextHelper.IsDiscountAllowed(this.priceContext, saleItem.ItemId) && saleItem.Quantity > 0)
         {
             // Add a new total discount
             DiscountLine totalDiscountItem = new DiscountLine
             {
                 DiscountLineType   = DiscountLineType.ManualDiscount,
                 ManualDiscountType = ManualDiscountType.TotalDiscountPercent,
                 Percentage         = transaction.TotalManualDiscountPercentage,
             };
             saleItem.DiscountLines.Add(totalDiscountItem);
             SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d));
         }
     }
 }
Example #16
0
            /// <summary>
            /// The calculation of a customer line discount.
            /// </summary>
            /// <param name="tradeAgreements">Trade agreement collection to calculate on. If null, uses the pricing data manager to find agreements.</param>
            /// <param name="transaction">The sales transaction which needs line discounts..</param>
            /// <returns>
            /// The sales transaction.
            /// </returns>
            public SalesTransaction CalcLineDiscount(
                List <TradeAgreement> tradeAgreements,
                SalesTransaction transaction)
            {
                // Loop trough all items all calc line discount
                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                if (tradeAgreements != null && tradeAgreements.Any())
                {
                    foreach (var saleItem in transaction.PriceCalculableSalesLines)
                    {
                        decimal absQty         = Math.Abs(saleItem.Quantity);
                        decimal discountAmount = 0m;
                        decimal percent1       = 0m;
                        decimal percent2       = 0m;
                        decimal minQty         = 0m;

                        this.GetLineDiscountLines(tradeAgreements, saleItem, ref absQty, ref discountAmount, ref percent1, ref percent2, ref minQty);

                        decimal totalPercentage = DiscountLine.GetCompoundedPercentage(percent1, percent2);

                        if ((totalPercentage != 0m) || (discountAmount != 0m))
                        {
                            DiscountLine discountItem = new DiscountLine
                            {
                                DiscountLineType     = DiscountLineType.CustomerDiscount,
                                CustomerDiscountType = CustomerDiscountType.LineDiscount,
                                Percentage           = totalPercentage,
                                Amount = discountAmount,
                            };

                            Discount.UpdateDiscountLines(saleItem, discountItem);
                        }
                    }
                }

                return(transaction);
            }
            private static void AllocateDiscountAmountToDiscountLines(SalesLine salesLine, LineDiscountCalculationType lineDiscountCalculationType, RoundingRule roundingRule)
            {
                if (salesLine.DiscountLines.Count == 0)
                {
                    return;
                }

                // customerLineAmountForMixOrMax and customerLinePercentForMixOrMax are for LineDiscountCalculationType.MaxLineMultiline and MinLineMultiline
                decimal customerLineAmountForMixOrMax  = 0;
                decimal customerLinePercentForMixOrMax = 0;

                List <DiscountLine> periodicDiscountItemList          = new List <DiscountLine>();
                List <DiscountLine> periodicThresholdDiscountItemList = new List <DiscountLine>();
                List <DiscountLine> customerLineDiscountItemList      = new List <DiscountLine>();

                // Manual line discount at most one.
                DiscountLine        manualLineDiscountItem  = null;
                List <DiscountLine> totalDiscountItemList   = new List <DiscountLine>();
                List <DiscountLine> loyaltyDiscountLineList = new List <DiscountLine>();

                //// Step 1: split discount lines into 5 groups: Periodic less threshold, Periodic threshold, Customer line, Manual line and Total and Loyalty
                ////         and figure out customerLineAmountForMixOrMax & customerLinePercentForMixOrMax along the way.
                foreach (DiscountLine discountLineItem in salesLine.DiscountLines)
                {
                    discountLineItem.FixInvalidAmountAndPercentage();

                    if (discountLineItem.DiscountLineType == DiscountLineType.CustomerDiscount)
                    {
                        // Customer Total
                        if (discountLineItem.CustomerDiscountType == CustomerDiscountType.TotalDiscount)
                        {
                            totalDiscountItemList.Add(discountLineItem);
                        }
                        else
                        {
                            // Customer Line
                            if (salesLine.LineMultilineDiscOnItem == LineMultilineDiscountOnItem.Both)
                            {
                                if (customerLineAmountForMixOrMax == decimal.Zero ||
                                    (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && discountLineItem.Amount > customerLineAmountForMixOrMax) ||
                                    (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && discountLineItem.Amount > 0 && discountLineItem.Amount < customerLineAmountForMixOrMax))
                                {
                                    customerLineAmountForMixOrMax = discountLineItem.Amount;
                                }

                                if (customerLinePercentForMixOrMax == decimal.Zero ||
                                    (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && discountLineItem.Percentage > customerLinePercentForMixOrMax) ||
                                    (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && discountLineItem.Percentage > 0 && discountLineItem.Percentage < customerLinePercentForMixOrMax))
                                {
                                    customerLinePercentForMixOrMax = discountLineItem.Percentage;
                                }
                            }

                            customerLineDiscountItemList.Add(discountLineItem);
                        }
                    }
                    else if (discountLineItem.DiscountLineType == DiscountLineType.PeriodicDiscount)
                    {
                        // Periodic a.k.a. Retail
                        if (discountLineItem.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold)
                        {
                            periodicThresholdDiscountItemList.Add(discountLineItem);
                        }
                        else
                        {
                            periodicDiscountItemList.Add(discountLineItem);
                        }
                    }
                    else if (discountLineItem.DiscountLineType == DiscountLineType.ManualDiscount &&
                             (discountLineItem.ManualDiscountType == ManualDiscountType.LineDiscountAmount ||
                              discountLineItem.ManualDiscountType == ManualDiscountType.LineDiscountPercent))
                    {
                        // Line Manual
                        manualLineDiscountItem = discountLineItem;
                    }
                    else if (discountLineItem.DiscountLineType == DiscountLineType.ManualDiscount &&
                             (discountLineItem.ManualDiscountType == ManualDiscountType.TotalDiscountAmount ||
                              discountLineItem.ManualDiscountType == ManualDiscountType.TotalDiscountPercent))
                    {
                        // Total manual
                        totalDiscountItemList.Add(discountLineItem);
                    }
                    else if (discountLineItem.DiscountLineType == DiscountLineType.LoyaltyDiscount)
                    {
                        // Loyalty discount
                        loyaltyDiscountLineList.Add(discountLineItem);
                    }
                }

                salesLine.PeriodicDiscount           = 0;
                salesLine.PeriodicPercentageDiscount = 0;

                // Step 2: allocate effective discount amount for periodic less threshold discount lines.
                AllocatePeriodicDiscountLines(salesLine, periodicDiscountItemList, roundingRule);

                // Step 3: allocate effective discount amount for periodic threshold discount lines.
                AllocatePeriodicDiscountLines(salesLine, periodicThresholdDiscountItemList, roundingRule);

                // Stpe 4: allocate effective discount amount for customer and manual line discounts.
                AllocateLineDiscountLines(salesLine, customerLineDiscountItemList, manualLineDiscountItem, lineDiscountCalculationType, customerLineAmountForMixOrMax, customerLinePercentForMixOrMax, roundingRule);

                // Stpe 4: allocate effective discount amount for total line discounts.
                AllocateTotalDiscountLines(salesLine, totalDiscountItemList, roundingRule);

                // Stpe 5: allocate effective discount amount for loyalty line discounts
                AllocateLoyaltyDiscountLines(salesLine, loyaltyDiscountLineList, roundingRule);
            }
Example #18
0
            /// <summary>
            /// This method will distribute the amountToDiscount across all the sale items in the transaction
            ///   proportionally except for the line item with the largest amount.  The remainder will be distributed
            ///   to the line item with the largest amount to ensure the amount to discount is exactly applied.
            /// This method currently works for either the customer discount or when the total discount button is applied.
            /// </summary>
            /// <param name="transaction">The transaction receiving total discount lines.</param>
            /// <param name="discountType">Whether this discount is for a customer or for the total discount item.</param>
            /// <param name="amountToDiscount">The amount to discount the transaction.</param>
            private void AddTotalDiscAmountLines(
                SalesTransaction transaction,
                DiscountLineType discountType,
                decimal amountToDiscount)
            {
                decimal totalAmtAvailableForDiscount = decimal.Zero;

                // Build a list of the discountable items with the largest value item last.
                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                var discountableSaleItems = (from s in transaction.PriceCalculableSalesLines
                                             where s.IsEligibleForDiscount() && s.Quantity > 0 &&
                                             PriceContextHelper.IsDiscountAllowed(this.priceContext, s.ItemId)
                                             orderby Math.Abs(s.NetAmount), s.LineId
                                             select s).ToList();

                // Iterate through all non voided items whether we are going to discount or not so that they get added
                // back to the totals
                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                foreach (var saleItem in transaction.PriceCalculableSalesLines)
                {
                    // We can clear the discount line for total discount because a total manual amount discount
                    // will override a total manual percent discount, whereas customer discount can have both
                    // amount and percentage applied simultaneously.
                    if (discountType == DiscountLineType.ManualDiscount)
                    {
                        Discount.ClearManualDiscountLinesOfType(saleItem, ManualDiscountType.TotalDiscountAmount);
                        Discount.ClearManualDiscountLinesOfType(saleItem, ManualDiscountType.TotalDiscountPercent);
                    }

                    SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d));

                    if (saleItem.IsEligibleForDiscount() && saleItem.Quantity > 0)
                    {
                        // Calculate the total amount that is available for discount
                        totalAmtAvailableForDiscount += Math.Abs(saleItem.NetAmountWithAllInclusiveTax);
                    }
                }

                // Calculate the percentage (as a fraction) that we should attempt to discount each discountable item
                // to reach the total.
                decimal discountFactor = totalAmtAvailableForDiscount != decimal.Zero ? (amountToDiscount / totalAmtAvailableForDiscount) : decimal.Zero;

                decimal totalAmtDistributed = decimal.Zero;

                // Iterate through all discountable items.
                foreach (var saleItem in discountableSaleItems)
                {
                    decimal amountToDiscountForThisItem = decimal.Zero;

                    if (saleItem != discountableSaleItems.Last())
                    {
                        // for every item except for the last in the list (which will have the largest value)
                        // discount by the rounded amount that is closest to the percentage desired for the transaction
                        decimal itemPrice = saleItem.NetAmount;
                        amountToDiscountForThisItem = this.priceContext.CurrencyAndRoundingHelper.Round(discountFactor * Math.Abs(itemPrice));
                        totalAmtDistributed        += amountToDiscountForThisItem;
                    }
                    else
                    {
                        // Discount the last item by the remainder to ensure that the exact desired discount is applied
                        amountToDiscountForThisItem = amountToDiscount - totalAmtDistributed;
                    }

                    DiscountLine discountItem;
                    if (amountToDiscountForThisItem != decimal.Zero)
                    {
                        if (discountType == DiscountLineType.ManualDiscount)
                        {
                            // Add a new total discount item
                            discountItem = new DiscountLine();
                            discountItem.DiscountLineType   = DiscountLineType.ManualDiscount;
                            discountItem.ManualDiscountType = ManualDiscountType.TotalDiscountAmount;
                            saleItem.DiscountLines.Add(discountItem);
                        }
                        else
                        {
                            // for customer discounts we need to either update the existing one, or add a new one.
                            discountItem = GetCustomerDiscountItem(saleItem, CustomerDiscountType.TotalDiscount, DiscountLineType.CustomerDiscount);
                        }

                        discountItem.Amount = saleItem.Quantity != 0 ? amountToDiscountForThisItem / saleItem.Quantity : amountToDiscountForThisItem;
                    }

                    SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d));
                }
            }
Example #19
0
            private static bool CanCompoundTogether(IEnumerable <DiscountLine> existingDiscountLines, DiscountLine newDiscountLine)
            {
                bool canCompoundTogether = true;

                if (!newDiscountLine.IsCompoundable)
                {
                    canCompoundTogether = false;
                }

                if (canCompoundTogether)
                {
                    if (newDiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold)
                    {
                        // threshold compounded discounts: can NOT compound across priorities within threshold discounts.
                        //                                 can compound on top of non-theshold discounts.
                        if (existingDiscountLines.Where(p => string.Equals(newDiscountLine.OfferId, p.OfferId, StringComparison.OrdinalIgnoreCase) ||
                                                        (p.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold && newDiscountLine.PricingPriorityNumber != p.PricingPriorityNumber) ||
                                                        !p.IsCompoundable).Any())
                        {
                            canCompoundTogether = false;
                        }
                    }
                    else
                    {
                        // non-threshold compounded discounts: can NOT compound across priorities.
                        if (existingDiscountLines.Where(p => string.Equals(newDiscountLine.OfferId, p.OfferId, StringComparison.OrdinalIgnoreCase) ||
                                                        newDiscountLine.PricingPriorityNumber != p.PricingPriorityNumber ||
                                                        !p.IsCompoundable).Any())
                        {
                            canCompoundTogether = false;
                        }
                    }
                }

                return(canCompoundTogether);
            }
Example #20
0
            /// <summary>
            /// The calculation of the total customer discount.
            /// </summary>
            /// <param name="tradeAgreements">Trade agreement collection to calculate on. If null, uses the pricing data manager to find agreements.</param>
            /// <param name="retailTransaction">The retail transaction which needs total discounts.</param>
            /// <returns>
            /// The retail transaction.
            /// </returns>
            public SalesTransaction CalcTotalCustomerDiscount(
                List <TradeAgreement> tradeAgreements,
                SalesTransaction retailTransaction)
            {
                if (tradeAgreements != null && tradeAgreements.Any())
                {
                    decimal totalAmount = 0;

                    // Find the total amount as a basis for the total discount
                    // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                    var clonedTransaction = retailTransaction.Clone <SalesTransaction>();

                    foreach (var clonedSalesLine in clonedTransaction.PriceCalculableSalesLines)
                    {
                        if (this.IsTotalDiscountAllowed(clonedSalesLine.ItemId))
                        {
                            SalesLineTotaller.CalculateLine(clonedTransaction, clonedSalesLine, d => this.priceContext.CurrencyAndRoundingHelper.Round(d));
                            totalAmount += clonedSalesLine.NetAmountWithAllInclusiveTax;
                        }
                    }

                    decimal absTotalAmount = Math.Abs(totalAmount);

                    // Find the total discounts.
                    PriceDiscountType        relation    = PriceDiscountType.EndDiscountSales; // Total sales discount - 7
                    PriceDiscountItemCode    itemCode    = PriceDiscountItemCode.AllItems;     // All items - 2
                    PriceDiscountAccountCode accountCode = 0;
                    string         itemRelation          = string.Empty;
                    decimal        percent1       = 0m;
                    decimal        percent2       = 0m;
                    decimal        discountAmount = 0m;
                    ProductVariant dimension      = new ProductVariant();

                    int idx = 0;
                    while (idx < /* Max(PriceDiscAccountCode) */ 3)
                    {   // Check discounts for Store Currency
                        accountCode = (PriceDiscountAccountCode)idx;

                        string accountRelation = string.Empty;
                        if (accountCode == PriceDiscountAccountCode.Customer)
                        {
                            accountRelation = retailTransaction.CustomerId;
                        }
                        else if (accountCode == PriceDiscountAccountCode.CustomerGroup)
                        {
                            accountRelation = this.priceContext.CustomerTotalPriceGroup;
                        }

                        accountRelation = accountRelation ?? string.Empty;

                        // Only get Active discount combinations
                        if (this.discountParameters.Activation(relation, (PriceDiscountAccountCode)accountCode, (PriceDiscountItemCode)itemCode))
                        {
                            var priceDiscTable = Discount.GetPriceDiscData(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, absTotalAmount, this.priceContext, dimension, false);

                            foreach (TradeAgreement row in priceDiscTable)
                            {
                                percent1       += row.PercentOne;
                                percent2       += row.PercentTwo;
                                discountAmount += row.Amount;

                                if (!row.ShouldSearchAgain)
                                {
                                    idx = 3;
                                }
                            }
                        }

                        idx++;
                    }

                    decimal totalPercentage = DiscountLine.GetCompoundedPercentage(percent1, percent2);

                    if (discountAmount != decimal.Zero)
                    {
                        this.AddTotalDiscAmountLines(retailTransaction, DiscountLineType.CustomerDiscount, discountAmount);
                    }

                    if (totalPercentage != 0)
                    {
                        // Update the sale items.
                        // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                        foreach (var saleItem in retailTransaction.PriceCalculableSalesLines)
                        {
                            if (this.IsTotalDiscountAllowed(saleItem.ItemId))
                            {
                                DiscountLine discountItem = GetCustomerDiscountItem(saleItem, CustomerDiscountType.TotalDiscount, DiscountLineType.CustomerDiscount);
                                discountItem.Percentage = totalPercentage;
                            }
                        }
                    }
                }

                return(retailTransaction);
            }
 /// <summary>
 /// Initializes a new instance of the <see cref="DiscountLineQuantity" /> class.
 /// </summary>
 /// <param name="discountLine">Discount line.</param>
 /// <param name="quantity">The quantity.</param>
 public DiscountLineQuantity(DiscountLine discountLine, decimal quantity)
 {
     this.DiscountLine = discountLine;
     this.Quantity     = quantity;
 }
Example #22
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));
                                    }
                                }
                            }
                        }
                    }
                }
            }
            /// <summary>
            /// Updates the sales transaction with the threshold promotion if applicable.
            /// </summary>
            /// <param name="existingTransaction">Existing transaction.</param>
            /// <param name="tempSalesTransaction">Copy of existing transaction.</param>
            /// <param name="context">The request context.</param>
            /// <param name="salesLineIndex">The sales line under consideration.</param>
            /// <param name="cartPromotionLines">The object with the cart promotion lines.</param>
            /// <param name="thresholdDiscount">The threshold discount line under consideration.</param>
            /// <param name="tiers">The tiers for the threshold discount.</param>
            private static void GetThresholdDiscounts(
                SalesTransaction existingTransaction,
                SalesTransaction tempSalesTransaction,
                RequestContext context,
                int salesLineIndex,
                Collection <string> cartPromotionLines,
                DiscountLine thresholdDiscount,
                IEnumerable <ThresholdDiscountTier> tiers)
            {
                // Find all the sales lines with the same offer.
                List <SalesLine> salesLinesWithOffer = tempSalesTransaction.SalesLines.Where(j => j.DiscountLines.Any(k => k.OfferId.Equals(thresholdDiscount.OfferId))).ToList();
                decimal          totalAmount         = salesLinesWithOffer.Select(j => j.GrossAmount).Sum();
                decimal          currentQuantity     = tempSalesTransaction.SalesLines[salesLineIndex].Quantity;

                // Find the minimum threshold amount required to hit a discount among all the tiers for this offer.
                IEnumerable <ThresholdDiscountTier> tiersForCurrentAmtOffer = tiers.Where(j => j.OfferId.Equals(thresholdDiscount.OfferId) && j.AmountThreshold > totalAmount).OrderBy(l => l.AmountThreshold);
                ThresholdDiscountTier tier = tiersForCurrentAmtOffer.Any() ? tiersForCurrentAmtOffer.First() : null;

                if (tier != null)
                {
                    // Add that amount difference to the first item that has this offer in the cart by increasing its quantity and see if this discount applies after applying concurrency rules.
                    existingTransaction.SalesLines[salesLineIndex].Quantity =
                        Math.Ceiling(tempSalesTransaction.SalesLines[salesLineIndex].Quantity *
                                     (tier.AmountThreshold - totalAmount + tempSalesTransaction.SalesLines[salesLineIndex].GrossAmount) / (tempSalesTransaction.SalesLines[salesLineIndex].GrossAmount / tempSalesTransaction.SalesLines[salesLineIndex].Quantity));

                    CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All);
                    DiscountLine isApplied = existingTransaction.SalesLines[salesLineIndex].DiscountLines.Where(j => j.OfferId.Equals(thresholdDiscount.OfferId)).SingleOrDefault();

                    if (isApplied != null)
                    {
                        var getItemsRequest = new GetItemsDataRequest(salesLinesWithOffer.Select(j => j.ItemId))
                        {
                            QueryResultSettings = new QueryResultSettings(new ColumnSet("NAME"), PagingInfo.AllRecords)
                        };
                        var getItemsResponse = context.Runtime.Execute <GetItemsDataResponse>(getItemsRequest, context);

                        ReadOnlyCollection <Item> items  = getItemsResponse.Items;
                        StringBuilder             buffer = new StringBuilder();

                        foreach (Item item in items.ToList())
                        {
                            buffer.Append(item.Name).Append(", ");
                        }

                        buffer.Remove(buffer.Length - 2, 1);

                        if (tier.DiscountMethod == ThresholdDiscountMethod.AmountOff)
                        {
                            thresholdDiscount.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.ThresholdDiscountPricePromotion, buffer, Math.Round(tier.AmountThreshold, 2), Math.Round(tier.DiscountValue, 2));
                        }
                        else
                        {
                            thresholdDiscount.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.ThresholdDiscountPercentagePromotion, buffer, Math.Round(tier.AmountThreshold, 2), Math.Round(tier.DiscountValue, 2));
                        }

                        cartPromotionLines.Add(thresholdDiscount.OfferName);
                    }
                }

                existingTransaction.SalesLines[salesLineIndex].Quantity = currentQuantity;
                CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All);

                foreach (SalesLine salesLineWithOffer in salesLinesWithOffer)
                {
                    salesLineWithOffer.DiscountLines.Remove(salesLineWithOffer.DiscountLines.Where(k => k.OfferId == thresholdDiscount.OfferId).SingleOrDefault());
                }
            }
Example #24
0
            /// <summary>
            /// Calculate the manual line discount.
            /// </summary>
            /// <param name="transaction">The transaction receiving total discount lines.</param>
            /// <param name="saleItem">The sale item that contains the discount lines.</param>
            /// <param name="lineDiscountItem">The line discount amount to discount the transaction.</param>
            private void AddLineDiscount(SalesTransaction transaction, SalesLine saleItem, DiscountLine lineDiscountItem)
            {
                Item item = PriceContextHelper.GetItem(this.priceContext, saleItem.ItemId);
                bool isDiscountAllowed = item != null ? !item.NoDiscountAllowed : true;

                if (isDiscountAllowed)
                {
                    saleItem.DiscountLines.Add(lineDiscountItem);
                    SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d));
                }
            }
            private static decimal AllocateLineDiscountLines(
                SalesLine salesLine,
                List <DiscountLine> customerLineDiscountItemList,
                DiscountLine manualLineDiscountItem,
                LineDiscountCalculationType lineDiscountCalculationType,
                decimal customerLineAmountForMixOrMax,
                decimal customerLinePercentForMixOrMax,
                RoundingRule roundingRule)
            {
                decimal lineDiscountEffectiveAmount     = 0;
                decimal lineDiscountEffectivePercentage = 0;
                decimal grossAmountDiscountable         = (salesLine.Price * salesLine.Quantity) - salesLine.PeriodicDiscount;

                if (grossAmountDiscountable != decimal.Zero)
                {
                    // Round 1: amount off first for customer line discounts and manual line discount, in that order.
                    if (customerLineDiscountItemList.Any())
                    {
                        foreach (DiscountLine customerLine in customerLineDiscountItemList)
                        {
                            if (salesLine.LineMultilineDiscOnItem != LineMultilineDiscountOnItem.Both ||
                                lineDiscountCalculationType == LineDiscountCalculationType.LinePlusMultiline ||
                                lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && customerLine.Amount == customerLineAmountForMixOrMax) ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && customerLine.Amount == customerLineAmountForMixOrMax) ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.Line && customerLine.CustomerDiscountType == CustomerDiscountType.LineDiscount) ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.Multiline && customerLine.CustomerDiscountType == CustomerDiscountType.MultilineDiscount))
                            {
                                customerLine.SetEffectiveAmountForAmountOff(grossAmountDiscountable - lineDiscountEffectiveAmount, salesLine.Quantity, roundingRule);
                                lineDiscountEffectiveAmount += customerLine.EffectiveAmount;

                                // In case of Min and Max, customerLineAmountForMixOrMax can only be applied once.
                                customerLineAmountForMixOrMax = decimal.Zero;
                            }
                            else
                            {
                                customerLine.EffectiveAmount = decimal.Zero;
                            }
                        }
                    }

                    if (manualLineDiscountItem != null)
                    {
                        manualLineDiscountItem.SetEffectiveAmountForAmountOff(grossAmountDiscountable - lineDiscountEffectiveAmount, salesLine.Quantity, roundingRule);
                        lineDiscountEffectiveAmount += manualLineDiscountItem.EffectiveAmount;
                    }

                    decimal grossAmountDiscountableLessAmountOff         = grossAmountDiscountable - lineDiscountEffectiveAmount;
                    decimal lineDiscountEffectiveAmountForPercentageOnly = 0;

                    // Round 2: percentage off for customer line discounts and manual line discount, in that order.
                    if (customerLineDiscountItemList.Any())
                    {
                        foreach (DiscountLine customerLine in customerLineDiscountItemList)
                        {
                            if (salesLine.LineMultilineDiscOnItem != LineMultilineDiscountOnItem.Both ||
                                lineDiscountCalculationType == LineDiscountCalculationType.LinePlusMultiline ||
                                lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && customerLine.Percentage == customerLinePercentForMixOrMax) ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && customerLine.Percentage == customerLinePercentForMixOrMax) ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.Line && customerLine.CustomerDiscountType == CustomerDiscountType.LineDiscount) ||
                                (lineDiscountCalculationType == LineDiscountCalculationType.Multiline && customerLine.CustomerDiscountType == CustomerDiscountType.MultilineDiscount))
                            {
                                // Compound only when LineDiscountCalculationType.LineMultiplyMultiline.
                                decimal grossAmountBase = grossAmountDiscountableLessAmountOff;
                                if (lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline)
                                {
                                    grossAmountBase = grossAmountDiscountableLessAmountOff - lineDiscountEffectiveAmountForPercentageOnly;
                                }

                                decimal maxDiscountAmount   = grossAmountDiscountable - lineDiscountEffectiveAmount;
                                decimal discountAmountAdded = customerLine.AddEffectiveAmountForPercentOff(grossAmountBase, maxDiscountAmount, roundingRule);

                                lineDiscountEffectiveAmount += discountAmountAdded;
                                lineDiscountEffectiveAmountForPercentageOnly += discountAmountAdded;

                                // In case of Min and Max, customerLineAmountForMixOrMax can only be applied once.
                                customerLineAmountForMixOrMax = decimal.Zero;
                            }
                        }
                    }

                    if (manualLineDiscountItem != null)
                    {
                        // Whether to add or to compound for manual discount follows the same rule for customer line and multiline discount.
                        decimal grossAmountBase = grossAmountDiscountableLessAmountOff;
                        if (lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline)
                        {
                            grossAmountBase = grossAmountDiscountableLessAmountOff - lineDiscountEffectiveAmountForPercentageOnly;
                        }

                        decimal maxDiscountAmount   = grossAmountDiscountable - lineDiscountEffectiveAmount;
                        decimal discountAmountAdded = manualLineDiscountItem.AddEffectiveAmountForPercentOff(grossAmountBase, maxDiscountAmount, roundingRule);

                        lineDiscountEffectiveAmount += discountAmountAdded;
                        lineDiscountEffectiveAmountForPercentageOnly += discountAmountAdded;
                    }

                    lineDiscountEffectivePercentage = (lineDiscountEffectiveAmount / grossAmountDiscountable) * 100m;
                }

                salesLine.LineDiscount           = lineDiscountEffectiveAmount;
                salesLine.LinePercentageDiscount = Math.Round(lineDiscountEffectivePercentage, 2);

                return(lineDiscountEffectiveAmount);
            }
Example #26
0
            public override AppliedDiscountApplication GetAppliedDiscountApplication(
                DiscountableItemGroup[] discountableItemGroups,
                decimal[] remainingQuantities,
                IEnumerable <AppliedDiscountApplication> appliedDiscounts,
                DiscountApplication discountApplication,
                PriceContext priceContext)
            {
                if (discountApplication == null || !discountApplication.RetailDiscountLines.Any() || discountableItemGroups == null || remainingQuantities == null)
                {
                    return(null);
                }

                decimal[] prices = new decimal[discountableItemGroups.Length];
                Dictionary <int, IList <DiscountLineQuantity> > discountDictionary = this.GetExistingDiscountDictionaryAndDiscountedPrices(
                    discountableItemGroups,
                    remainingQuantities,
                    appliedDiscounts,
                    discountApplication,
                    true,
                    true,
                    prices);

                RetailDiscountLineItem retailDiscountLineItem = discountApplication.RetailDiscountLines.ElementAt(0);

                DiscountOfferMethod discountMethod = (DiscountOfferMethod)retailDiscountLineItem.RetailDiscountLine.DiscountMethod;
                decimal             dealPrice      = decimal.Zero;

                decimal discountValue = decimal.Zero;
                decimal discountAmountForDiscountLine = decimal.Zero;

                switch (discountMethod)
                {
                case DiscountOfferMethod.DiscountAmount:
                    discountValue = retailDiscountLineItem.RetailDiscountLine.DiscountAmount;
                    discountAmountForDiscountLine = discountValue;
                    break;

                case DiscountOfferMethod.DiscountPercent:
                    discountValue = prices[retailDiscountLineItem.ItemIndex] * (retailDiscountLineItem.RetailDiscountLine.DiscountPercent / 100M);
                    break;

                case DiscountOfferMethod.OfferPrice:
                    dealPrice = retailDiscountLineItem.RetailDiscountLine.OfferPrice;
                    decimal bestExistingDealPrice = 0m;
                    bool    hasExistingDealPrice  = DiscountBase.TryGetBestExistingDealPrice(discountDictionary, retailDiscountLineItem.ItemIndex, out bestExistingDealPrice);

                    // We don't use discounted price here.
                    discountValue = DiscountBase.GetDiscountAmountFromDealPrice(discountableItemGroups[retailDiscountLineItem.ItemIndex].Price, hasExistingDealPrice, bestExistingDealPrice, dealPrice);
                    discountAmountForDiscountLine = discountValue;
                    break;

                default:
                    break;
                }

                // When has no competing discounts or compounded, apply all remaining quantity.
                bool    applyAllAvailableQuantity = (discountApplication.ApplyStandalone || this.CanCompound) && !discountApplication.HonorQuantity;
                decimal quantityToApply           = applyAllAvailableQuantity ?
                                                    remainingQuantities[retailDiscountLineItem.ItemIndex] : discountApplication.ItemQuantities[retailDiscountLineItem.ItemIndex];

                decimal result = discountValue * quantityToApply;

                AppliedDiscountApplication newAppliedDiscountApplication = null;

                if (result > decimal.Zero)
                {
                    Dictionary <int, decimal> itemQuantities;

                    if (applyAllAvailableQuantity)
                    {
                        itemQuantities = new Dictionary <int, decimal>();
                        itemQuantities[retailDiscountLineItem.ItemIndex] = quantityToApply;
                    }
                    else
                    {
                        itemQuantities = discountApplication.ItemQuantities;
                    }

                    newAppliedDiscountApplication = new AppliedDiscountApplication(discountApplication, result, itemQuantities, isDiscountLineGenerated: true);

                    DiscountLine discountLine = this.NewDiscountLine(discountApplication.DiscountCode, discountableItemGroups[retailDiscountLineItem.ItemIndex].ItemId);

                    discountLine.PeriodicDiscountType = PeriodicDiscountOfferType.Offer;
                    discountLine.DealPrice            = dealPrice;
                    discountLine.Amount     = discountAmountForDiscountLine;
                    discountLine.Percentage = retailDiscountLineItem.RetailDiscountLine.DiscountPercent;

                    newAppliedDiscountApplication.AddDiscountLine(retailDiscountLineItem.ItemIndex, new DiscountLineQuantity(discountLine, itemQuantities[retailDiscountLineItem.ItemIndex]));

                    if (discountApplication.RemoveItemsFromLookupsWhenApplied)
                    {
                        this.RemoveItemIndexGroupFromLookups(retailDiscountLineItem.ItemIndex);
                    }
                }

                return(newAppliedDiscountApplication);
            }
Example #27
0
            /// <summary>
            /// This method will distribute the amountToDiscount across all the sale items in the transaction
            ///     proportionally except for the line item with the largest amount.  The remainder will be distributed
            ///     to the line item with the largest amount to ensure the amount to discount is exactly applied.
            /// This method currently works when the redeem loyalty points button is applied.
            /// </summary>
            /// <param name="transaction">The transaction receiving loyalty discount lines.</param>
            /// <param name="amountToDiscount">The amount to discount the transaction.</param>
            private void AddLoyaltyDiscAmountLines(
                SalesTransaction transaction,
                decimal amountToDiscount)
            {
                decimal totalAmtAvailableForDiscount = decimal.Zero;

                // Build a list of the discountable items with the largest value item last
                var discountableSaleItems = (from s in transaction.SalesLines
                                             where ((s.IsEligibleForDiscount() && PriceContextHelper.IsDiscountAllowed(this.priceContext, s.ItemId)) ||
                                                    s.IsLoyaltyDiscountApplied)
                                             orderby Math.Abs(s.NetAmount), s.LineId
                                             select s).ToList();

                // Iterate through all non voided items whether we are going to discount or not so that they get added
                // back to the totals
                foreach (SalesLine salesLine in transaction.SalesLines.Where(sl => !sl.IsVoided))
                {
                    Discount.ClearDiscountLinesOfType(salesLine, DiscountLineType.LoyaltyDiscount);
                    SalesLineTotaller.CalculateLine(transaction, salesLine, d => this.priceContext.CurrencyAndRoundingHelper.Round(d));

                    if (salesLine.IsEligibleForDiscount() || salesLine.IsLoyaltyDiscountApplied)
                    {
                        // Calculate the total amount that is available for discount
                        totalAmtAvailableForDiscount += Math.Abs(salesLine.NetAmountWithAllInclusiveTax);
                    }
                }

                // Calculate the percentage (as a fraction) that we should attempt to discount each discountable item
                // to reach the total
                decimal discountFactor = totalAmtAvailableForDiscount != decimal.Zero ? (amountToDiscount / totalAmtAvailableForDiscount) : decimal.Zero;

                decimal totalAmtDistributed = decimal.Zero;

                // Iterate through all discountable items.
                foreach (SalesLine salesLine in discountableSaleItems)
                {
                    decimal amountToDiscountForThisItem = decimal.Zero;

                    if (salesLine != discountableSaleItems.Last())
                    {
                        // for every item except for the last in the list (which will have the largest value)
                        // discount by the rounded amount that is closest to the percentage desired for the transaction
                        decimal itemPrice = salesLine.NetAmount;
                        amountToDiscountForThisItem = this.priceContext.CurrencyAndRoundingHelper.Round(discountFactor * Math.Abs(itemPrice));
                        totalAmtDistributed        += amountToDiscountForThisItem;
                    }
                    else
                    {
                        // Discount the last item by the remainder to ensure that the exact desired discount is applied
                        amountToDiscountForThisItem = amountToDiscount - totalAmtDistributed;
                    }

                    DiscountLine discountLine;
                    if (amountToDiscountForThisItem != decimal.Zero)
                    {
                        // Add a new loyalty points discount item
                        discountLine                  = new DiscountLine();
                        discountLine.Amount           = salesLine.Quantity != 0 ? amountToDiscountForThisItem / salesLine.Quantity : amountToDiscountForThisItem;
                        discountLine.DiscountLineType = DiscountLineType.LoyaltyDiscount;

                        salesLine.DiscountLines.Add(discountLine);
                        salesLine.IsLoyaltyDiscountApplied = true;
                    }

                    SalesLineTotaller.CalculateLine(transaction, salesLine, d => this.priceContext.CurrencyAndRoundingHelper.Round(d));
                }
            }
            private static decimal AllocatePeriodicDiscountLines(SalesLine salesLine, List <DiscountLine> periodicDiscountItemList, RoundingRule roundingRule)
            {
                decimal periodicDiscountEffectiveAmount = 0;
                decimal periodicDiscountPercentage      = salesLine.PeriodicPercentageDiscount;
                decimal quantityDiscounted      = salesLine.Quantity;
                decimal grossAmountDiscountable = (salesLine.Price * quantityDiscounted) - salesLine.PeriodicDiscount;
                decimal totalPeriodicDiscountEffectiveAmount = salesLine.PeriodicDiscount;

                if (periodicDiscountItemList.Any() && grossAmountDiscountable != decimal.Zero)
                {
                    // Round 0: figure out amount for deal price, in which best price wins.
                    DiscountLine lineWithBestDealPrice = null;
                    foreach (DiscountLine periodicLine in periodicDiscountItemList)
                    {
                        if (periodicLine.DealPrice > decimal.Zero)
                        {
                            if (lineWithBestDealPrice == null)
                            {
                                lineWithBestDealPrice = periodicLine;
                            }
                            else if (periodicLine.DealPrice < lineWithBestDealPrice.DealPrice)
                            {
                                // Sets the amount = 0 on existing lineWithBestDealPrice, and then resets lineWithBestDealPrice.
                                lineWithBestDealPrice.Amount = 0;
                                lineWithBestDealPrice        = periodicLine;
                            }
                            else
                            {
                                periodicLine.Amount = decimal.Zero;
                            }
                        }
                    }

                    if (lineWithBestDealPrice != null)
                    {
                        lineWithBestDealPrice.SetAmountForDealPrice(grossAmountDiscountable - periodicDiscountEffectiveAmount, quantityDiscounted, roundingRule);
                    }

                    // Round 1: amount off second.
                    foreach (DiscountLine periodicLine in periodicDiscountItemList)
                    {
                        periodicDiscountEffectiveAmount += periodicLine.SetEffectiveAmountForAmountOff(grossAmountDiscountable - periodicDiscountEffectiveAmount, quantityDiscounted, roundingRule);
                    }

                    // Round 2: percent off. If we have 2 periodic discounts applied to a single line, they much be compounded.
                    foreach (DiscountLine periodicLine in periodicDiscountItemList)
                    {
                        if (grossAmountDiscountable != periodicDiscountEffectiveAmount)
                        {
                            decimal grossAmountBase   = grossAmountDiscountable - periodicDiscountEffectiveAmount;
                            decimal maxDiscountAmount = grossAmountBase;
                            periodicDiscountEffectiveAmount += periodicLine.AddEffectiveAmountForPercentOff(grossAmountBase, maxDiscountAmount, roundingRule);
                        }
                    }

                    totalPeriodicDiscountEffectiveAmount += periodicDiscountEffectiveAmount;
                    periodicDiscountPercentage            = (totalPeriodicDiscountEffectiveAmount / grossAmountDiscountable) * 100m;
                }

                salesLine.PeriodicDiscount           = totalPeriodicDiscountEffectiveAmount;
                salesLine.PeriodicPercentageDiscount = Math.Round(periodicDiscountPercentage, 2);

                return(periodicDiscountEffectiveAmount);
            }