Пример #1
0
            /// <summary>
            /// Create a new instance of the <see cref="PriceContext"/> class.
            /// </summary>
            /// <param name="requestContext">Request context.</param>
            /// <param name="pricingDataManager">Pricing data manager.</param>
            /// <param name="priceParameters">Price parameters.</param>
            /// <param name="currencyAndRoundingHelper">Currency and rounding helper.</param>
            /// <param name="itemIds">Item Ids.</param>
            /// <param name="catalogIds">Catalog identifiers.</param>
            /// <param name="activeDate">Active date.</param>
            /// <param name="priceCalculationMode">Price calculation mode.</param>
            /// <param name="discountCalculationMode">Discount calculation mode.</param>
            /// <returns>A new instance of the <see cref="PriceContext"/> class.</returns>
            public static PriceContext CreatePriceContext(
                RequestContext requestContext,
                IPricingDataAccessor pricingDataManager,
                PriceParameters priceParameters,
                ICurrencyOperations currencyAndRoundingHelper,
                ISet <string> itemIds,
                ISet <long> catalogIds,
                DateTimeOffset activeDate,
                PricingCalculationMode priceCalculationMode,
                DiscountCalculationMode discountCalculationMode)
            {
                if (requestContext == null)
                {
                    throw new ArgumentNullException("requestContext");
                }

                PriceContext priceContext = new PriceContext
                {
                    CurrencyAndRoundingHelper = currencyAndRoundingHelper,
                    ActiveDate              = activeDate,
                    PriceParameters         = priceParameters,
                    PriceCalculationMode    = priceCalculationMode,
                    DiscountCalculationMode = discountCalculationMode,
                };

                PriceContextHelper.InitializePriceContextOfInferredProperties(priceContext, pricingDataManager, requestContext, itemIds, catalogIds, null);

                return(priceContext);
            }
            private Dictionary <string, decimal> GetPriceGroupSalesQuantityLookup(SalesTransaction transaction)
            {
                // Sum up all the linegroup discount lines in the same group
                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                Dictionary <string, decimal> priceGroupSalesQuantityLookup = new Dictionary <string, decimal>(StringComparer.OrdinalIgnoreCase);

                foreach (SalesLine salesLine in transaction.PriceCalculableSalesLines)
                {
                    Item item = PriceContextHelper.GetItem(this.priceContext, salesLine.ItemId);
                    if (item != null && !string.IsNullOrEmpty(item.MultilineDiscountGroupId))
                    {
                        decimal quantity = decimal.Zero;

                        if (priceGroupSalesQuantityLookup.TryGetValue(item.MultilineDiscountGroupId, out quantity))
                        {
                            priceGroupSalesQuantityLookup[item.MultilineDiscountGroupId] = quantity + salesLine.Quantity;
                        }
                        else
                        {
                            priceGroupSalesQuantityLookup.Add(item.MultilineDiscountGroupId, salesLine.Quantity);
                        }
                    }
                }

                return(priceGroupSalesQuantityLookup);
            }
Пример #3
0
            private bool IsTotalDiscountAllowed(string itemId)
            {
                Item item = PriceContextHelper.GetItem(this.priceContext, itemId);
                bool isTotalDiscountAllowed = item != null && item.IsTotalDiscountAllowed && !item.NoDiscountAllowed;

                return(isTotalDiscountAllowed);
            }
Пример #4
0
            private static ReadOnlyCollection <PeriodicDiscount> GetRetailDiscounts(
                IEnumerable <SalesLine> salesLines,
                PriceContext priceContext,
                IPricingDataAccessor pricingDataManager,
                QueryResultSettings settings)
            {
                // don't do lookup if there aren't any price groups to search by
                HashSet <string> allPriceGroups = PriceContextHelper.GetAllPriceGroupsForDiscount(priceContext);

                if (allPriceGroups.Count == 0)
                {
                    return(new ReadOnlyCollection <PeriodicDiscount>(new PeriodicDiscount[0]));
                }

                var items = salesLines.Select(l => new ItemUnit
                {
                    ItemId = l.ItemId,
                    VariantInventoryDimensionId = l.InventoryDimensionId ?? string.Empty,
                    UnitOfMeasure = Discount.GetUnitOfMeasure(l),
                });

                ReadOnlyCollection <PeriodicDiscount> discounts =
                    pricingDataManager.ReadRetailDiscounts(items, allPriceGroups, priceContext.ActiveDate, priceContext.ActiveDate, priceContext.CurrencyCode, settings) as ReadOnlyCollection <PeriodicDiscount>;

                ReadOnlyCollection <PeriodicDiscount> validDiscounts =
                    discounts.Where(d => InternalValidationPeriod.ValidateDateAgainstValidationPeriod((DateValidationType)d.DateValidationType, d.ValidationPeriod, d.ValidFromDate, d.ValidToDate, priceContext.ActiveDate)).AsReadOnly();

                return(validDiscounts);
            }
Пример #5
0
            /// <summary>
            /// Check whether the discount is allowed for the item.
            /// </summary>
            /// <param name="priceContext">Price context.</param>
            /// <param name="itemId">Item identifier.</param>
            /// <returns>True if  the discount is allowed for the item, otherwise false.</returns>
            public static bool IsDiscountAllowed(PriceContext priceContext, string itemId)
            {
                Item item = PriceContextHelper.GetItem(priceContext, itemId);
                bool isDiscountAllowed = item != null ? !item.NoDiscountAllowed : true;

                return(isDiscountAllowed);
            }
            /// <summary>
            /// The calculation of a customer multiline discount.
            /// </summary>
            /// <remarks>
            /// Calculation of multiline discount is done as follows:
            ///   1. Create working table for calculation.
            ///   2. Populate working table with total quantities for all the multiline groups encountered on the sales lines.
            ///   3. For all rows (and therefore multiline groups) found, search for trade agreements in the database.
            ///      a. The search is first for customer-specific, then customer multiline discount group, then all customers.
            ///      b. The search stops when a trade agreement is encountered with "Find next" unmarked.
            ///      c. If nothing is found for the store currency the search is attempted again with the company accounting currency.
            ///   4. All found agreements are summed in the working table and applied to each sales line which matches the multiline groups.
            ///   5. If there are sales lines which weren't discounted with a multiline discount.
            ///      a. Find their total quantity and search for any multiline trade agreements marked for "All items".
            ///      b. If any agreements were found apply them to any lines that weren't already discounted with a multiline discount.
            /// </remarks>
            /// <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 multiline discounts attached.</param>
            /// <returns>
            /// The sales transaction.
            /// </returns>
            public SalesTransaction CalcMultiLineDiscount(List <TradeAgreement> tradeAgreements, SalesTransaction transaction)
            {
                if (tradeAgreements != null && tradeAgreements.Any())
                {
                    // collection of salesLine not discounted by multiline discount group
                    // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                    var nondiscountedSalesLines = new List <SalesLine>(transaction.PriceCalculableSalesLines);

                    Dictionary <string, decimal> priceGroupSalesQuantityLookup = this.GetPriceGroupSalesQuantityLookup(transaction);

                    decimal percent1       = decimal.Zero;
                    decimal percent2       = decimal.Zero;
                    decimal discountAmount = decimal.Zero;

                    // Find discounts for the different multiline discount groups
                    foreach (KeyValuePair <string, decimal> priceGroupQuantityPair in priceGroupSalesQuantityLookup)
                    {
                        // we've found some multiline discount groups, so clear non-discounted lines from the default of "all"
                        nondiscountedSalesLines.Clear();

                        // find multiline discounts for this multiline discount row
                        this.GetMultiLineDiscountLine(tradeAgreements, PriceDiscountItemCode.ItemGroup, transaction, priceGroupQuantityPair.Key, priceGroupQuantityPair.Value, out percent1, out percent2, out discountAmount);

                        // Update the sale items.
                        // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                        foreach (var saleItem in transaction.PriceCalculableSalesLines)
                        {
                            Item   item            = PriceContextHelper.GetItem(this.priceContext, saleItem.ItemId);
                            string discountGroupId = item != null ? item.MultilineDiscountGroupId : string.Empty;
                            if (string.Equals(discountGroupId, priceGroupQuantityPair.Key, StringComparison.OrdinalIgnoreCase))
                            {
                                // if line is part of discounted item group, apply the discount
                                ApplyMultilineDiscount(saleItem, percent1, percent2, discountAmount);
                            }
                            else
                            {
                                // otherwise, add to non-discounted lines
                                nondiscountedSalesLines.Add(saleItem);
                            }
                        }
                    }

                    // find total quantity of items on lines still eligible for multiline discount
                    decimal lineSum = nondiscountedSalesLines.Aggregate(0M, (acc, sl) => acc + sl.Quantity);

                    // find any multiline discounts to apply to "all items"
                    this.GetMultiLineDiscountLine(tradeAgreements, PriceDiscountItemCode.AllItems, transaction, string.Empty, lineSum, out percent1, out percent2, out discountAmount);

                    // Update the sale items.
                    foreach (var saleItem in nondiscountedSalesLines)
                    {
                        ApplyMultilineDiscount(saleItem, percent1, percent2, discountAmount);
                    }
                }

                return(transaction);
            }
Пример #7
0
            /// <summary>
            /// This method will calculate the prices for the whole sales transaction.
            /// </summary>
            /// <param name="salesTransaction">Sales transaction.</param>
            /// <param name="pricingDataManager">Provides access to the pricing data to the pricing calculation.</param>
            /// <param name="currencyAndRoundingHelper">Currency and rounding helper.</param>
            /// <param name="customerPriceGroup">Customer price group.</param>
            /// <param name="currencyCode">Current code.</param>
            /// <param name="activeDate">Active date time offset for price.</param>
            /// <remarks>Parallel processing has been disabled, but we leave parameter here for backward compatibility.</remarks>
            public static void CalculatePricesForTransaction(
                SalesTransaction salesTransaction,
                IPricingDataAccessor pricingDataManager,
                ICurrencyOperations currencyAndRoundingHelper,
                string customerPriceGroup,
                string currencyCode,
                DateTimeOffset activeDate)
            {
                if (salesTransaction == null)
                {
                    throw new ArgumentNullException("salesTransaction");
                }

                if (pricingDataManager == null)
                {
                    throw new ArgumentNullException("pricingDataManager");
                }

                if (currencyAndRoundingHelper == null)
                {
                    throw new ArgumentNullException("currencyAndRoundingHelper");
                }

                ISet <long> catalogIds = PriceContextHelper.GetCatalogIds(salesTransaction);
                IEnumerable <AffiliationLoyaltyTier> affiliationLoyaltyTiers = PriceContextHelper.GetAffiliationLoyalTierIds(salesTransaction);

                ISet <string> itemIds      = PriceContextHelper.GetItemIds(salesTransaction);
                PriceContext  priceContext = PriceContextHelper.CreatePriceContext(
                    pricingDataManager,
                    currencyAndRoundingHelper,
                    PricingCalculationMode.Transaction,
                    DiscountCalculationMode.None,
                    itemIds,
                    catalogIds,
                    affiliationLoyaltyTiers,
                    salesTransaction.CustomerId,
                    customerPriceGroup,
                    salesTransaction.IsTaxIncludedInPrice,
                    currencyCode,
                    activeDate);

                bool isDiagnosticsCollected = GetCollectDiagnostics(salesTransaction);

                if (isDiagnosticsCollected)
                {
                    priceContext.IsDiagnosticsCollected         = true;
                    priceContext.PricingEngineDiagnosticsObject = new PricingEngineDiagnosticsObject();
                }

                PricingEngine.CalculatePricesForSalesLines(salesTransaction.PriceCalculableSalesLines, priceContext, pricingDataManager);

                if (isDiagnosticsCollected)
                {
                    SetPricingEngineDiagnosticsObject(salesTransaction, priceContext.PricingEngineDiagnosticsObject);
                }
            }
Пример #8
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));
                }
            }
Пример #9
0
            /// <summary>
            /// For all sales lines on the transaction, retrieve the product rec id if it's not already set.
            /// </summary>
            /// <param name="pricingDataManager">Provides data access to the calculation.</param>
            /// <param name="priceContext">Price context.</param>
            /// <param name="salesLines">Sales lines.</param>
            private static void PopulateProductIds(IPricingDataAccessor pricingDataManager, PriceContext priceContext, IEnumerable <SalesLine> salesLines)
            {
                var itemVariantIds = new HashSet <ItemVariantInventoryDimension>();

                foreach (var line in salesLines)
                {
                    if ((line.Variant == null || line.Variant.DistinctProductVariantId == 0) && !string.IsNullOrWhiteSpace(line.InventoryDimensionId))
                    {
                        var itemVariantId = new ItemVariantInventoryDimension(line.ItemId, line.InventoryDimensionId);
                        itemVariantIds.Add(itemVariantId);
                    }
                }

                // We make a single database call to retrieve all variant identifiers that we need
                // and create a map using the ItemVariantInventoryDimension as its key.
                var variantsMap = new Dictionary <ItemVariantInventoryDimension, ProductVariant>();

                if (itemVariantIds.Any())
                {
                    variantsMap = ((IEnumerable <ProductVariant>)pricingDataManager.GetVariants(itemVariantIds)).ToDictionary(key => new ItemVariantInventoryDimension(key.ItemId, key.InventoryDimensionId));
                }

                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                foreach (var line in salesLines)
                {
                    if (line.MasterProductId == 0)
                    {
                        Item item = PriceContextHelper.GetItem(priceContext, line.ItemId);

                        line.MasterProductId = (item != null) ? item.Product : 0L;

                        if (item != null && string.IsNullOrWhiteSpace(line.OriginalSalesOrderUnitOfMeasure))
                        {
                            line.OriginalSalesOrderUnitOfMeasure = item.SalesUnitOfMeasure;
                        }
                    }

                    if ((line.Variant == null || line.Variant.DistinctProductVariantId == 0) && !string.IsNullOrWhiteSpace(line.InventoryDimensionId))
                    {
                        ProductVariant variant;
                        var            itemVariant = new ItemVariantInventoryDimension(line.ItemId, line.InventoryDimensionId);
                        if (variantsMap.TryGetValue(itemVariant, out variant))
                        {
                            line.Variant = variant;
                        }
                    }

                    if (line.ProductId == 0)
                    {
                        line.ProductId = line.Variant != null ? line.Variant.DistinctProductVariantId : line.MasterProductId;
                    }
                }
            }
            /// <summary>
            /// Implements the IPricingCalculator interface to calculate item price trade agreement prices.
            /// </summary>
            /// <param name="salesLines">The item lines which need prices.</param>
            /// <param name="priceContext">The configuration of the overall pricing context for the calculation.</param>
            /// <param name="pricingDataManager">Instance of pricing data manager to access pricing data.</param>
            /// <returns>Sets of possible price lines keyed by item line Id.</returns>
            public Dictionary <string, IEnumerable <PriceLine> > CalculatePriceLines(
                IEnumerable <SalesLine> salesLines,
                PriceContext priceContext,
                IPricingDataAccessor pricingDataManager)
            {
                Tuple <DateTimeOffset, DateTimeOffset> dateRange = PricingEngine.GetMinAndMaxActiveDates(salesLines, priceContext.ActiveDate);

                // look up all trade agreements for given items and context
                HashSet <string> itemIds = new HashSet <string>(salesLines.Select(s => s.ItemId).Distinct(), StringComparer.OrdinalIgnoreCase);
                ReadOnlyCollection <TradeAgreement> tradeAgreements = pricingDataManager.ReadPriceTradeAgreements(
                    itemIds,
                    PriceContextHelper.GetAllPriceGroupsForPrice(priceContext),
                    priceContext.CustomerAccount,
                    dateRange.Item1,
                    dateRange.Item2,
                    priceContext.CurrencyCode,
                    QueryResultSettings.AllRecords) as ReadOnlyCollection <TradeAgreement>;

                if (priceContext.IsDiagnosticsCollected && tradeAgreements.Any())
                {
                    priceContext.PricingEngineDiagnosticsObject.AddTradeAgreementsConsidered(tradeAgreements.ToList());
                }

                var agreementsByItemId = IndexAgreementsByItemId(tradeAgreements);

                var discountParameters = DiscountParameters.CreateAndInitialize(priceContext.PriceParameters);

                Dictionary <string, IEnumerable <PriceLine> > itemPriceLines;
                Dictionary <string, decimal> itemQuantites = null;

                if (priceContext.PriceCalculationMode == PricingCalculationMode.Transaction)
                {
                    itemQuantites = GetItemQuantities(salesLines);
                }

                itemPriceLines = new Dictionary <string, IEnumerable <PriceLine> >(StringComparer.OrdinalIgnoreCase);

                foreach (SalesLine salesLine in salesLines)
                {
                    Tuple <decimal, string> priceCustPriceGroup = CalculateAgreementPriceLine(salesLines, salesLine, priceContext, agreementsByItemId, discountParameters, itemQuantites);
                    if (priceCustPriceGroup.Item1 != decimal.Zero)
                    {
                        itemPriceLines.Add(salesLine.LineId, new List <PriceLine>(1)
                        {
                            ConstructTradeAgreementPriceLine(priceCustPriceGroup)
                        });
                    }
                }

                return(itemPriceLines);
            }
Пример #11
0
            /// <summary>
            /// Create a new instance of the <see cref="PriceContext"/> class.
            /// </summary>
            /// <param name="requestContext">Request context.</param>
            /// <param name="pricingDataManager">Pricing data manager.</param>
            /// <param name="transaction">Current transaction.</param>
            /// <param name="currencyAndRoundingHelper">Currency and rounding helper.</param>
            /// <param name="activeDate">Active date.</param>
            /// <param name="customerId">Customer Id.</param>
            /// <param name="customerPriceGroup">Customer price group.</param>
            /// <param name="customerLinePriceGroup">Customer line discount price group.</param>
            /// <param name="customerMultipleLinePriceGroup">Customer multiple line discount price group.</param>
            /// <param name="customerTotalPriceGroup">Customer total discount price group.</param>
            /// <param name="priceIncludesTax">Price includes tax.</param>
            /// <param name="priceCalculationMode">Price calculation mode.</param>
            /// <param name="discountCalculationMode">Discount calculation mode.</param>
            /// <param name="calculateForNewSalesLinesOnly">A flag indicating whether to calculate for new sales lines only.</param>
            /// <param name="newSalesLineIdSet">New sales line id set.</param>
            /// <returns>A new instance of the <see cref="PriceContext"/> class.</returns>
            public static PriceContext CreatePriceContext(
                RequestContext requestContext,
                IPricingDataAccessor pricingDataManager,
                SalesTransaction transaction,
                ICurrencyOperations currencyAndRoundingHelper,
                DateTimeOffset activeDate,
                string customerId,
                string customerPriceGroup,
                string customerLinePriceGroup,
                string customerMultipleLinePriceGroup,
                string customerTotalPriceGroup,
                bool priceIncludesTax,
                PricingCalculationMode priceCalculationMode,
                DiscountCalculationMode discountCalculationMode,
                bool calculateForNewSalesLinesOnly,
                HashSet <string> newSalesLineIdSet)
            {
                if (requestContext == null)
                {
                    throw new ArgumentNullException("requestContext");
                }

                PriceContext priceContext = new PriceContext
                {
                    CurrencyAndRoundingHelper = currencyAndRoundingHelper,
                    ActiveDate                     = activeDate,
                    CustomerAccount                = customerId,
                    CustomerPriceGroup             = customerPriceGroup,
                    CustomerLinePriceGroup         = customerLinePriceGroup,
                    CustomerMultipleLinePriceGroup = customerMultipleLinePriceGroup,
                    CustomerTotalPriceGroup        = customerTotalPriceGroup,
                    IsTaxInclusive                 = priceIncludesTax,
                    PriceCalculationMode           = priceCalculationMode,
                    DiscountCalculationMode        = discountCalculationMode,
                    CalculateForNewSalesLinesOnly  = calculateForNewSalesLinesOnly,
                };

                if (newSalesLineIdSet != null && newSalesLineIdSet.Count > 0)
                {
                    priceContext.NewSalesLineIdSet.AddRange(newSalesLineIdSet);
                }

                ISet <long> catalogIds = GetCatalogIds(transaction);
                IEnumerable <AffiliationLoyaltyTier> affiliationLoyaltyTiers = GetAffiliationLoyalTierIds(transaction);
                ISet <string> itemIds = GetItemIds(transaction);

                PriceContextHelper.InitializePriceContextOfInferredProperties(priceContext, pricingDataManager, requestContext, itemIds, catalogIds, affiliationLoyaltyTiers);

                return(priceContext);
            }
Пример #12
0
            /// <summary>
            /// Create a new instance of the <see cref="PriceContext"/> class for price or discount calculation.
            /// </summary>
            /// <param name="pricingDataManager">Pricing data manager.</param>
            /// <param name="currencyAndRoundingHelper">Currency and rounding helper.</param>
            /// <param name="priceCalculationMode">Price calculation mode.</param>
            /// <param name="discountCalculationMode">Discount calculation mode.</param>
            /// <param name="itemIds">Item Ids.</param>
            /// <param name="catalogIds">Catalog identifiers.</param>
            /// <param name="affiliationLoyaltyTiers">Affiliation or loyalty tier identifiers.</param>
            /// <param name="customerId">Customer Id.</param>
            /// <param name="customerPriceGroup">Customer price group.</param>
            /// <param name="customerLinePriceGroup">Customer line discount price group.</param>
            /// <param name="customerMultipleLinePriceGroup">Customer multiple line discount price group.</param>
            /// <param name="customerTotalPriceGroup">Customer total discount price group.</param>
            /// <param name="priceIncludesTax">Price includes tax.</param>
            /// <param name="currencyCode">Currency code.</param>
            /// <param name="activeDate">Active date.</param>
            /// <returns>A new instance of the <see cref="PriceContext"/> class.</returns>
            public static PriceContext CreatePriceContext(
                IPricingDataAccessor pricingDataManager,
                ICurrencyOperations currencyAndRoundingHelper,
                PricingCalculationMode priceCalculationMode,
                DiscountCalculationMode discountCalculationMode,
                ISet <string> itemIds,
                ISet <long> catalogIds,
                IEnumerable <AffiliationLoyaltyTier> affiliationLoyaltyTiers,
                string customerId,
                string customerPriceGroup,
                string customerLinePriceGroup,
                string customerMultipleLinePriceGroup,
                string customerTotalPriceGroup,
                bool priceIncludesTax,
                string currencyCode,
                DateTimeOffset activeDate)
            {
                if (pricingDataManager == null)
                {
                    throw new ArgumentNullException("pricingDataManager");
                }

                PriceContext priceContext = new PriceContext
                {
                    CurrencyAndRoundingHelper = currencyAndRoundingHelper,
                    ActiveDate                     = activeDate,
                    CurrencyCode                   = currencyCode,
                    CustomerAccount                = customerId,
                    CustomerPriceGroup             = customerPriceGroup,
                    CustomerLinePriceGroup         = customerLinePriceGroup,
                    CustomerMultipleLinePriceGroup = customerMultipleLinePriceGroup,
                    CustomerTotalPriceGroup        = customerTotalPriceGroup,
                    PriceParameters                = pricingDataManager.GetPriceParameters(),
                    IsTaxInclusive                 = priceIncludesTax,
                    PriceCalculationMode           = priceCalculationMode,
                    DiscountCalculationMode        = discountCalculationMode,
                };

                PriceContextHelper.InitializePriceContexOfAlgorithmMode(priceContext, pricingDataManager);
                PriceContextHelper.InitializePriceContexOfPriceGroups(priceContext, pricingDataManager, catalogIds, affiliationLoyaltyTiers);
                PriceContextHelper.InitializeItemCache(priceContext, pricingDataManager, itemIds);

                return(priceContext);
            }
Пример #13
0
            internal static Dictionary <long, List <DiscountBase> > GetProductOrVarintToDiscountMapFromCache(
                IPricingDataAccessor pricingDataManager,
                PriceContext priceContext,
                SalesTransaction transaction)
            {
                ISet <long> productVariantMasterIdsInTransaction = GetProductVariantMasterIdsForTransaction(transaction);
                Dictionary <long, IList <RetailCategoryMember> > categorytoProductOrVariantIdsMap = GetCategoryToProductOrVariantIdsMapForTransaction(pricingDataManager, productVariantMasterIdsInTransaction);

                IEnumerable <RetailDiscount> allDiscounts = pricingDataManager.GetAllRetailDiscounts() as IEnumerable <RetailDiscount>;

                Dictionary <long, List <DiscountBase> > allApplicableDiscounts = new Dictionary <long, List <DiscountBase> >();

                foreach (RetailDiscount retailDiscount in allDiscounts)
                {
                    if (!PriceContextHelper.MatchCalculationMode(priceContext, retailDiscount.PeriodicDiscountType))
                    {
                        continue;
                    }

                    DiscountBase discount = ConvertRetailDiscountToDiscountBase(retailDiscount, priceContext);
                    discount.ProductOrVariantIdsInTransaction = productVariantMasterIdsInTransaction;

                    // Product or variant id to categories map is needed to filter which discount lines are applicable for the transaction. See DiscountBase class.
                    discount.CategoryToProductOrVariantIdsMap = categorytoProductOrVariantIdsMap;
                    IDictionary <long, IList <RetailDiscountLine> > itemDiscounts = discount.GetProductOrVariantIdToRetailDiscountLinesMap();
                    foreach (long productOrVariantId in itemDiscounts.Keys)
                    {
                        if (allApplicableDiscounts.ContainsKey(productOrVariantId))
                        {
                            allApplicableDiscounts[productOrVariantId].Add(discount);
                        }
                        else
                        {
                            allApplicableDiscounts.Add(productOrVariantId, new List <DiscountBase>()
                            {
                                discount
                            });
                        }
                    }
                }

                return(allApplicableDiscounts);
            }
Пример #14
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));
         }
     }
 }
Пример #15
0
            /// <summary>
            /// Gets list of discount lines corresponding to products.
            /// </summary>
            /// <param name="items">The collection of products.</param>
            /// <param name="priceContext">The pricing context.</param>
            /// <param name="pricingDataManager">The Pricing data manager.</param>
            /// <param name="settings">Query Settings.</param>
            /// <returns>List of discounts.</returns>
            /// <remarks>The method returns flattened list of discounts which roughly speaking means: it returns 1 line per each possible discount for every product.</remarks>
            private static ReadOnlyCollection <PeriodicDiscount> GetRetailDiscountsAndLines(
                IEnumerable <ItemUnit> items,
                PriceContext priceContext,
                IPricingDataAccessor pricingDataManager,
                QueryResultSettings settings)
            {
                // don't do lookup if there aren't any price groups to search by
                HashSet <string> allPriceGroups = PriceContextHelper.GetAllPriceGroupsForDiscount(priceContext);

                if (allPriceGroups.Count == 0)
                {
                    return(new ReadOnlyCollection <PeriodicDiscount>(new PeriodicDiscount[0]));
                }

                ReadOnlyCollection <PeriodicDiscount> discounts =
                    pricingDataManager.ReadRetailDiscounts(items, allPriceGroups, priceContext.ActiveDate, priceContext.ActiveDate, priceContext.CurrencyCode, settings) as ReadOnlyCollection <PeriodicDiscount>;

                ReadOnlyCollection <PeriodicDiscount> validDiscounts = discounts.Where(p => InternalValidationPeriod.ValidateDateAgainstValidationPeriod((DateValidationType)p.DateValidationType, p.ValidationPeriod, p.ValidFromDate, p.ValidToDate, priceContext.ActiveDate)).AsReadOnly();

                return(validDiscounts);
            }
            private static Tuple <decimal, string> CalculateAgreementPriceLine(
                IEnumerable <SalesLine> salesLines,
                SalesLine salesLine,
                PriceContext priceContext,
                Dictionary <string, IList <TradeAgreement> > agreementsByItemId,
                DiscountParameters discountParameters,
                Dictionary <string, decimal> itemQuantites)
            {
                var quantity = salesLine.Quantity;

                // count all occurrences for this item if this is a transaction
                if (priceContext.PriceCalculationMode == PricingCalculationMode.Transaction)
                {
                    itemQuantites.TryGetValue(salesLine.ItemId, out quantity);

                    if (quantity == decimal.Zero)
                    {
                        quantity = 1m;
                    }
                }

                var activeDate = (salesLine.SalesDate != null) ? salesLine.SalesDate.Value : priceContext.ActiveDate;

                return(GetActiveTradeAgreementPriceAndGroup(
                           agreementsByItemId,
                           discountParameters,
                           priceContext.CurrencyCode,
                           salesLine.ItemId,
                           salesLine.OriginalSalesOrderUnitOfMeasure,
                           Discount.GetUnitOfMeasure(salesLine),
                           salesLine.Variant,
                           salesLine.UnitOfMeasureConversion,
                           quantity,
                           priceContext.CustomerAccount,
                           priceContext.CustomerPriceGroup,
                           PriceContextHelper.GetApplicablePriceGroupsForPrice(priceContext, salesLine.CatalogIds),
                           salesLines,
                           priceContext,
                           activeDate));
            }
Пример #17
0
            /// <summary>
            /// Create a new instance of the <see cref="PriceContext"/> class.
            /// </summary>
            /// <param name="requestContext">Request context.</param>
            /// <param name="pricingDataManager">Pricing data manager.</param>
            /// <param name="transaction">Current transaction.</param>
            /// <param name="priceParameters">Price parameters.</param>
            /// <param name="currencyAndRoundingHelper">Currency and rounding helper.</param>
            /// <param name="activeDate">Active date.</param>
            /// <param name="customerId">Customer Id.</param>
            /// <param name="customerPriceGroup">Customer price group.</param>
            /// <param name="priceIncludesTax">Price includes tax.</param>
            /// <param name="priceCalculationMode">Price calculation mode.</param>
            /// <param name="discountCalculationMode">Discount calculation mode.</param>
            /// <returns>A new instance of the <see cref="PriceContext"/> class.</returns>
            public static PriceContext CreatePriceContext(
                RequestContext requestContext,
                IPricingDataAccessor pricingDataManager,
                SalesTransaction transaction,
                PriceParameters priceParameters,
                ICurrencyOperations currencyAndRoundingHelper,
                DateTimeOffset activeDate,
                string customerId,
                string customerPriceGroup,
                bool priceIncludesTax,
                PricingCalculationMode priceCalculationMode,
                DiscountCalculationMode discountCalculationMode)
            {
                if (requestContext == null)
                {
                    throw new ArgumentNullException("requestContext");
                }

                PriceContext priceContext = new PriceContext
                {
                    CurrencyAndRoundingHelper = currencyAndRoundingHelper,
                    ActiveDate              = activeDate,
                    CustomerAccount         = customerId,
                    CustomerPriceGroup      = customerPriceGroup,
                    IsTaxInclusive          = priceIncludesTax,
                    PriceParameters         = priceParameters,
                    PriceCalculationMode    = priceCalculationMode,
                    DiscountCalculationMode = discountCalculationMode,
                };

                ISet <long> catalogIds = GetCatalogIds(transaction);
                IEnumerable <AffiliationLoyaltyTier> affiliationLoyaltyTiers = GetAffiliationLoyalTierIds(transaction);
                ISet <string> itemIds = GetItemIds(transaction);

                PriceContextHelper.InitializePriceContextOfInferredProperties(priceContext, pricingDataManager, requestContext, itemIds, catalogIds, affiliationLoyaltyTiers);

                return(priceContext);
            }
Пример #18
0
            internal static Dictionary <long, List <DiscountBase> > GetProductOrVariantToDiscountMapLive(
                SalesTransaction transaction,
                PriceContext priceContext,
                IPricingDataAccessor pricingDataManager)
            {
                List <ItemUnit>           items = new List <ItemUnit>();
                Dictionary <string, long> itemIdInventDimIdToProductOrVariantIdMap = new Dictionary <string, long>(StringComparer.OrdinalIgnoreCase);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                    SetEffectiveDiscountPriorityFromPriceGroups(offerIdToDiscountMap, priceContext);

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

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

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

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

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

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

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

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

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

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

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

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

                return(productDiscountMap);
            }
Пример #19
0
            internal static PriceContext BuildPriceContext(
                IPricingDataAccessor pricingDataManager,
                ICurrencyOperations currencyAndRoundingHelper,
                Customer customer,
                SalesTransaction transaction,
                string currencyCode,
                bool doesPriceIncludeTax,
                DiscountCalculationMode discountCalculationMode,
                DateTimeOffset activeDate)
            {
                string customerAccount                = string.Empty;
                string customerPriceGroup             = string.Empty;
                string customerLinePriceGroup         = string.Empty;
                string customerMultipleLinePriceGroup = string.Empty;
                string customerTotalPriceGroup        = string.Empty;

                if (customer != null)
                {
                    if (!string.IsNullOrWhiteSpace(customer.AccountNumber))
                    {
                        customerAccount = customer.AccountNumber;
                    }

                    if (!string.IsNullOrWhiteSpace(customer.PriceGroup))
                    {
                        customerPriceGroup = customer.PriceGroup;
                    }

                    if (!string.IsNullOrWhiteSpace(customer.LineDiscountGroup))
                    {
                        customerLinePriceGroup = customer.LineDiscountGroup;
                    }

                    if (!string.IsNullOrWhiteSpace(customer.MultilineDiscountGroup))
                    {
                        customerMultipleLinePriceGroup = customer.MultilineDiscountGroup;
                    }

                    if (!string.IsNullOrWhiteSpace(customer.TotalDiscountGroup))
                    {
                        customerTotalPriceGroup = customer.TotalDiscountGroup;
                    }
                }

                ISet <string> itemIds    = PriceContextHelper.GetItemIds(transaction);
                ISet <long>   catalogIds = PriceContextHelper.GetCatalogIds(transaction);
                IEnumerable <AffiliationLoyaltyTier> affiliationLoyaltyTierIds = PriceContextHelper.GetAffiliationLoyalTierIds(transaction);

                return(PriceContextHelper.CreatePriceContext(
                           pricingDataManager,
                           currencyAndRoundingHelper,
                           PricingCalculationMode.Transaction,
                           discountCalculationMode,
                           itemIds,
                           catalogIds,
                           affiliationLoyaltyTierIds,
                           customerAccount,
                           customerPriceGroup,
                           customerLinePriceGroup,
                           customerMultipleLinePriceGroup,
                           customerTotalPriceGroup,
                           doesPriceIncludeTax,
                           currencyCode ?? string.Empty,
                           activeDate));
            }
Пример #20
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));
                }
            }
Пример #21
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));
                                    }
                                }
                            }
                        }
                    }
                }
            }
Пример #22
0
            /// <summary>
            /// Implements the IPricingCalculator interface to calculate item price adjustment prices.
            /// </summary>
            /// <param name="salesLines">The item lines which need prices.</param>
            /// <param name="priceContext">The configuration of the overall pricing context for the calculation.</param>
            /// <param name="pricingDataManager">Instance of pricing data manager to access pricing data.</param>
            /// <returns>Sets of possible price lines keyed by item line Id.</returns>
            public Dictionary <string, IEnumerable <PriceLine> > CalculatePriceLines(
                IEnumerable <SalesLine> salesLines,
                PriceContext priceContext,
                IPricingDataAccessor pricingDataManager)
            {
                // get pairs of product Id and distinct variant Id from lines to search by
                var productIds = salesLines
                                 .Select(sl => new ItemUnit
                {
                    ItemId = sl.ItemId,
                    VariantInventoryDimensionId = sl.InventoryDimensionId ?? string.Empty,
                    DistinctProductVariant      = sl.Variant != null ? sl.Variant.DistinctProductVariantId : 0,
                    Product = sl.MasterProductId == 0 ? sl.ProductId : sl.MasterProductId
                })
                                 .Distinct().ToList();

                Tuple <DateTimeOffset, DateTimeOffset> dateRange = PricingEngine.GetMinAndMaxActiveDates(salesLines, priceContext.ActiveDate);

                // datetime to use depends on listing/cart scenario
                if (priceContext.PriceCalculationMode == PricingCalculationMode.Independent)
                {
                    dateRange = new Tuple <DateTimeOffset, DateTimeOffset>(dateRange.Item1, dateRange.Item2);
                }

                // fetch all price adjustments (and any related validation periods)
                ISet <string> allPriceGroups = PriceContextHelper.GetAllPriceGroupsForPrice(priceContext);
                IEnumerable <PriceAdjustment> adjustments = pricingDataManager.ReadPriceAdjustments(
                    productIds,
                    allPriceGroups,
                    dateRange.Item1,
                    dateRange.Item2,
                    QueryResultSettings.AllRecords) as IEnumerable <PriceAdjustment>;

                if (priceContext.IsDiagnosticsCollected && adjustments.Any())
                {
                    priceContext.PricingEngineDiagnosticsObject.AddPriceAdjustmentsConsidered(adjustments.Select(x => x.OfferId).ToList());
                }

                HashSet <string> offerIds = new HashSet <string>();

                foreach (PriceAdjustment adjustment in adjustments)
                {
                    offerIds.Add(adjustment.OfferId);
                }

                IDictionary <string, IList <PriceGroup> > adjustmentPriceGroupDictionary = PriceContextHelper.GetRetailDiscountPriceGroupDictionaryFilteredByTransaction(
                    pricingDataManager,
                    offerIds,
                    allPriceGroups);

                FixAdjustmentPriorityFromPriceGroups(adjustments, adjustmentPriceGroupDictionary);

                // "index" adjustments by item Id & dimensions
                var adjustmentDict = adjustments
                                     .GroupBy(a => GetItemIdInventDimIdKey(a.ItemId, a.InventoryDimensionId))
                                     .ToDictionary(grp => grp.Key, grp => grp.ToList(), StringComparer.OrdinalIgnoreCase);

                var promotionLines = new Dictionary <string, IList <PriceAdjustment> >(StringComparer.OrdinalIgnoreCase);

                foreach (var line in salesLines)
                {
                    // get item price adjustments, continue to next item if none found
                    var key = GetItemIdInventDimIdKey(line.ItemId, line.InventoryDimensionId);

                    List <PriceAdjustment> itemAdjustments;
                    if (!adjustmentDict.TryGetValue(key, out itemAdjustments))
                    {
                        continue;
                    }

                    List <PriceAdjustment> allApplicablePriceAdjustmentsForItem = new List <PriceAdjustment>();
                    foreach (var a in itemAdjustments)
                    {
                        IList <PriceGroup> priceGroups = null;
                        adjustmentPriceGroupDictionary.TryGetValue(a.OfferId, out priceGroups);

                        HashSet <string> priceGroupIdSet = new HashSet <string>(StringComparer.OrdinalIgnoreCase);
                        priceGroupIdSet.AddRange(priceGroups.Select(p => p.GroupId));
                        if (PriceContextHelper.IsApplicableForPrice(priceContext, priceGroupIdSet, line.CatalogIds) &&
                            IsAdjustmentActiveOnSalesLine(line, a, priceContext.ActiveDate) &&
                            IsMatchUnitOfMeasure(line, a) &&
                            IsMatchCurrency(priceContext.CurrencyCode, a))
                        {
                            allApplicablePriceAdjustmentsForItem.Add(a);
                        }
                    }

                    if (allApplicablePriceAdjustmentsForItem.Any())
                    {
                        // We apply price adjustment from only those with highest priority.
                        int highestPriority = allApplicablePriceAdjustmentsForItem.Max(p => p.PricingPriorityNumber);
                        IEnumerable <PriceAdjustment> priceAdjustmentsWithHighestPriorityForItem = allApplicablePriceAdjustmentsForItem.Where(p => p.PricingPriorityNumber == highestPriority);

                        // filter price adjustments by active validation periods before adding to dictionary
                        foreach (var a in priceAdjustmentsWithHighestPriorityForItem)
                        {
                            IList <PriceAdjustment> promos;
                            if (!promotionLines.TryGetValue(line.LineId, out promos))
                            {
                                promos = new List <PriceAdjustment>();
                                promotionLines.Add(line.LineId, promos);
                            }

                            promos.Add(a);
                        }
                    }
                }

                // convert price adjustments to price lines for the sales lines
                var priceLines = PriceAdjustmentsToPriceLines(salesLines, promotionLines);

                return(priceLines);
            }
Пример #23
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));
                }
            }
Пример #24
0
            private void GetLineDiscountLines(
                List <TradeAgreement> tradeAgreements,
                SalesLine saleItem,
                ref decimal absQty,
                ref decimal discountAmount,
                ref decimal percent1,
                ref decimal percent2,
                ref decimal minQty)
            {
                int idx = 0;

                while (idx < 9)
                {
                    PriceDiscountItemCode    itemCode    = (PriceDiscountItemCode)(idx % 3); // Mod divsion
                    PriceDiscountAccountCode accountCode = (PriceDiscountAccountCode)(idx / 3);

                    string accountRelation = string.Empty;
                    if (accountCode == PriceDiscountAccountCode.Customer)
                    {
                        accountRelation = this.priceContext.CustomerAccount;
                    }
                    else if (accountCode == PriceDiscountAccountCode.CustomerGroup)
                    {
                        accountRelation = this.priceContext.CustomerLinePriceGroup;
                    }

                    accountRelation = accountRelation ?? string.Empty;

                    string itemRelation;
                    if (itemCode == PriceDiscountItemCode.Item)
                    {
                        itemRelation = saleItem.ItemId;
                    }
                    else
                    {
                        Item item = PriceContextHelper.GetItem(this.priceContext, saleItem.ItemId);
                        itemRelation = item != null ? item.LineDiscountGroupId : string.Empty;
                    }

                    itemRelation = itemRelation ?? string.Empty;

                    PriceDiscountType relation = PriceDiscountType.LineDiscountSales; // Sales line discount - 5

                    if (this.discountParameters.Activation(relation, accountCode, itemCode))
                    {
                        if (DiscountParameters.ValidRelation(accountCode, accountRelation) &&
                            DiscountParameters.ValidRelation(itemCode, itemRelation))
                        {
                            bool dimensionDiscountFound = false;

                            if (saleItem.Variant != null && !string.IsNullOrEmpty(saleItem.Variant.VariantId))
                            {
                                var dimensionPriceDiscTable = Discount.GetPriceDiscData(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, absQty, this.priceContext, saleItem.Variant, true);

                                foreach (TradeAgreement row in dimensionPriceDiscTable)
                                {
                                    bool unitsAreUndefinedOrEqual =
                                        string.IsNullOrEmpty(row.UnitOfMeasureSymbol) ||
                                        string.Equals(row.UnitOfMeasureSymbol, saleItem.SalesOrderUnitOfMeasure, StringComparison.OrdinalIgnoreCase);

                                    if (unitsAreUndefinedOrEqual)
                                    {
                                        percent1       += row.PercentOne;
                                        percent2       += row.PercentTwo;
                                        discountAmount += row.Amount;
                                        minQty         += row.QuantityAmountFrom;
                                    }

                                    if (percent1 > 0M || percent2 > 0M || discountAmount > 0M)
                                    {
                                        dimensionDiscountFound = true;
                                    }

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

                            if (!dimensionDiscountFound)
                            {
                                var priceDiscTable = Discount.GetPriceDiscData(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, absQty, this.priceContext, saleItem.Variant, false);

                                foreach (TradeAgreement row in priceDiscTable)
                                {
                                    // Apply default if the unit of measure is not set from the cart.
                                    string unitOfMeasure = Discount.GetUnitOfMeasure(saleItem);

                                    bool unitsAreUndefinedOrEqual =
                                        string.IsNullOrEmpty(row.UnitOfMeasureSymbol) ||
                                        string.Equals(row.UnitOfMeasureSymbol, unitOfMeasure, StringComparison.OrdinalIgnoreCase);

                                    if (unitsAreUndefinedOrEqual)
                                    {
                                        percent1       += row.PercentOne;
                                        percent2       += row.PercentTwo;
                                        discountAmount += row.Amount;
                                        minQty         += row.QuantityAmountFrom;
                                    }

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

                    idx++;
                }
            }