internal static decimal GetUnitDiscountAmountAndCheckWhetherItsFullyCovered(RetailDiscountLine discountLineDefinition, decimal price, out bool isFullyCovered) { isFullyCovered = true; decimal unitDiscountAmount = decimal.Zero; switch ((DiscountOfferMethod)discountLineDefinition.DiscountMethod) { case DiscountOfferMethod.DiscountAmount: unitDiscountAmount = discountLineDefinition.DiscountAmount; isFullyCovered = unitDiscountAmount <= price; break; case DiscountOfferMethod.DiscountPercent: unitDiscountAmount = DiscountBase.GetDiscountAmountForPercentageOff(price, discountLineDefinition.DiscountPercent); break; case DiscountOfferMethod.OfferPrice: unitDiscountAmount = DiscountBase.GetDiscountAmountForDealUnitPrice(price, discountLineDefinition.OfferPrice); isFullyCovered = discountLineDefinition.OfferPrice <= price; break; default: break; } unitDiscountAmount = Math.Min(price, unitDiscountAmount); unitDiscountAmount = Math.Max(unitDiscountAmount, decimal.Zero); return(unitDiscountAmount); }
/// <summary> /// Prepares remaining quantities for thresholds for the first time. /// </summary> /// <param name="bestDiscountPath">Best discount path of application discount applications so far.</param> /// <param name="remainingQuantities">Remaining quantities for the next round.</param> /// <param name="remainingQuantitiesForCompound">Remaining quantities for compound for the next round.</param> /// <remarks>Threshold can be compounded on top on non-thresholds, so we have to reset remaining quantities before we evaluate threshold discounts.</remarks> internal void PrepareRemainingQuantitiesForThresholdsFirstTime( List <AppliedDiscountApplication> bestDiscountPath, decimal[] remainingQuantities, decimal[] remainingQuantitiesForCompound) { // Step 1: fill in this.ItemGroupIndexToCompoundedOfferToQuantityLookup from the last round. // reduce remaining quantitied for non-compounded discounts. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); foreach (AppliedDiscountApplication appliedDiscountApplication in bestDiscountPath) { DiscountBase discount = appliedDiscountApplication.DiscountApplication.Discount; if (!discount.CanCompound) { ReduceQuantitiesFromNonCompounded(appliedDiscountApplication, remainingQuantities, remainingQuantitiesForCompound); } } // Finish off items with threshold discounts. foreach (KeyValuePair <int, Dictionary <int, decimal> > pair in this.itemGroupIndexToPriorityToSettledCompoundedQuantityForThreshold) { int itemGroupIndex = pair.Key; // Compounded threshold always take the whole item. remainingQuantities[itemGroupIndex] = decimal.Zero; remainingQuantitiesForCompound[itemGroupIndex] = decimal.Zero; } // Process non-threshold compounded quantities. this.ReduceQuantitiesFromCompoundedNonThresholdForThreshold(remainingQuantities); // Clear current lookup. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); }
private static void SetEffectiveDiscounDiscountPriorityFromPriceGroups( DiscountBase discount, PriceContext priceContext) { if (discount.PricingPriorityNumber <= 0) { foreach (long priceGroupRecordId in discount.PriceDiscountGroupIds) { string priceGroupId = null; if (priceContext.RecordIdsToPriceGroupIdsDictionary.TryGetValue(priceGroupRecordId, out priceGroupId)) { int priority = 0; if (priceContext.PriceGroupIdToPriorityDictionary.TryGetValue(priceGroupId, out priority)) { if (priority > discount.PricingPriorityNumber) { // We could have a new property on DiscountBase that indicates effective priority number. // This is much simpler, for now, without complications. discount.PricingPriorityNumber = priority; } } } } } }
public override DiscountBase GetDiscountStrategy(Basket basket, DiscountBase parentDiscountStrategy = null) { DiscountBase bestStrategy = null; double bestPrice = 0; //Product Discount + MembershipDiscount DiscountBase productDiscount = new ProductDiscount(true); DiscountBase membershipAndProductDiscount = BasketDiscountOverMemberShipFactory.Instance.GetDiscountStrategy(basket, productDiscount); bestStrategy = membershipAndProductDiscount; bestPrice = membershipAndProductDiscount.GetDiscountedTotal(basket); //Basket Discount + MembershipDiscount DiscountBase basketDiscount = BasketDiscountOverTotalFactory.Instance.GetDiscountStrategy(basket, null); DiscountBase memberShipAndBasketDiscount = BasketDiscountOverMemberShipFactory.Instance.GetDiscountStrategy(basket, basketDiscount); (bestPrice, bestStrategy) = GetBetterStrategy(bestPrice, bestStrategy, basket, memberShipAndBasketDiscount); //Product Discount + CategoryDiscount + MembershipDiscount DiscountBase categoryAndProductDiscount = CategoryDiscountFactory.Instance.GetDiscountStrategy(basket, productDiscount); DiscountBase membershipCategoryAndProdutDiscount = BasketDiscountOverMemberShipFactory.Instance.GetDiscountStrategy(basket, categoryAndProductDiscount); (bestPrice, bestStrategy) = GetBetterStrategy(bestPrice, bestStrategy, basket, membershipCategoryAndProdutDiscount); //Special Offer Basket Discount (like BlackFriday) DiscountBase blackFridayDiscountStrategy = BlackFridayDiscountFactory.Instance.GetDiscountStrategy(basket, null); (bestPrice, bestStrategy) = GetBetterStrategy(bestPrice, bestStrategy, basket, blackFridayDiscountStrategy); return(bestStrategy); }
internal void FillLookup( DiscountBase discount, int itemGroupIndex, decimal quantity) { // Build the lookup from itemGroupId to OfferId to Quantity. Dictionary <string, decimal> offerIdToQuantityLookup = null; if (this.ItemGroupIndexToCompoundedOfferToQuantityLookup.TryGetValue(itemGroupIndex, out offerIdToQuantityLookup)) { decimal existingQuantity = decimal.Zero; if (offerIdToQuantityLookup.TryGetValue(discount.OfferId, out existingQuantity)) { offerIdToQuantityLookup[discount.OfferId] = quantity + existingQuantity; } else { offerIdToQuantityLookup.Add(discount.OfferId, quantity); } } else { offerIdToQuantityLookup = new Dictionary <string, decimal>(); offerIdToQuantityLookup.Add(discount.OfferId, quantity); this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Add(itemGroupIndex, offerIdToQuantityLookup); } }
/// <summary> /// Initializes a new instance of the <see cref="DiscountApplication" /> class. /// </summary> /// <param name="discount">The discount.</param> /// <param name="applyStandalone">Whether to apply it standalone.</param> /// <param name="removeItemsFromLookupsWhenApplied">Whether to remove items from lookups when applied.</param> public DiscountApplication(DiscountBase discount, bool applyStandalone, bool removeItemsFromLookupsWhenApplied) { this.Discount = discount; this.ApplyStandalone = applyStandalone; this.RemoveItemsFromLookupsWhenApplied = removeItemsFromLookupsWhenApplied; this.NumberOfTimesApplicable = discount != null ? discount.NumberOfTimesApplicable : 0; this.ItemQuantities = new Dictionary <int, decimal>(); }
/// <summary> /// Converts retail discount data from database to discount object. /// </summary> /// <param name="retailDiscount">Retail discount data from database.</param> /// <param name="priceContext">Price context.</param> /// <returns>Discount object.</returns> /// <remarks>This is private. Exposed as internal for test.</remarks> internal static DiscountBase ConvertRetailDiscountToDiscountBase(RetailDiscount retailDiscount, PriceContext priceContext) { DiscountBase discount = ConvertRetailDiscountToDiscountBase(retailDiscount); SetEffectiveDiscounDiscountPriorityFromPriceGroups(discount, priceContext); return(discount); }
private static DiscountBase ConvertDiscountAndLineToDiscountBase(PeriodicDiscount discountAndLine) { DiscountBase discount = null; OfferDiscount offer = null; MixAndMatchDiscount mixAndMatch = null; MultipleBuyDiscount multipleBuy = null; ThresholdDiscount threshold = null; switch (discountAndLine.PeriodicDiscountType) { case PeriodicDiscountOfferType.Offer: offer = new OfferDiscount(discountAndLine.ValidationPeriod); discount = offer; break; case PeriodicDiscountOfferType.MixAndMatch: mixAndMatch = new MixAndMatchDiscount(discountAndLine.ValidationPeriod); mixAndMatch.DealPriceValue = discountAndLine.MixAndMatchDealPrice; mixAndMatch.DiscountAmountValue = discountAndLine.MixAndMatchDiscountAmount; mixAndMatch.DiscountPercentValue = discountAndLine.MixAndMatchDiscountPercent; mixAndMatch.NumberOfLeastExpensiveLines = discountAndLine.MixAndMatchNumberOfLeastExpensiveLines; mixAndMatch.NumberOfTimesApplicable = discountAndLine.MixAndMatchNumberOfTimeApplicable; mixAndMatch.LeastExpensiveMode = discountAndLine.LeastExpensiveMode; discount = mixAndMatch; break; case PeriodicDiscountOfferType.MultipleBuy: multipleBuy = new MultipleBuyDiscount(discountAndLine.ValidationPeriod); discount = multipleBuy; break; case PeriodicDiscountOfferType.Threshold: threshold = new ThresholdDiscount(discountAndLine.ValidationPeriod); threshold.ShouldCountNonDiscountItems = discountAndLine.ShouldCountNonDiscountItems != 0; discount = threshold; break; } if (discount != null) { discount.IsCategoryToProductOrVariantIdsMapSet = true; discount.OfferId = discountAndLine.OfferId; discount.OfferName = discountAndLine.Name; discount.PeriodicDiscountType = discountAndLine.PeriodicDiscountType; discount.IsDiscountCodeRequired = discountAndLine.IsDiscountCodeRequired; discount.ConcurrencyMode = discountAndLine.ConcurrencyMode; discount.PricingPriorityNumber = discountAndLine.PricingPriorityNumber; discount.CurrencyCode = discountAndLine.CurrencyCode; discount.DateValidationPeriodId = discountAndLine.ValidationPeriodId; discount.DateValidationType = (DateValidationType)discountAndLine.DateValidationType; discount.DiscountType = GetDiscountMethodType(discount.PeriodicDiscountType, discountAndLine.DiscountType); discount.ValidFrom = discountAndLine.ValidFromDate; discount.ValidTo = discountAndLine.ValidToDate; } return(discount); }
private static void SetEffectiveDiscountPriorityFromPriceGroups( Dictionary <string, DiscountBase> discountsLookup, PriceContext priceContext) { foreach (KeyValuePair <string, DiscountBase> pair in discountsLookup) { DiscountBase discount = pair.Value; SetEffectiveDiscounDiscountPriorityFromPriceGroups(discount, priceContext); } }
private static bool BirthdayDiscountPredicate(DiscountBase discountBase, Customer customer, Product product) { var birthdayDiscount = (BirthdayDiscount)discountBase; bool DateOfBirthIsDiscountRange() => customer.DateOfBirth >= DateTimeOffset.Now - TimeSpan.FromDays(birthdayDiscount.DateRangeInDays) && customer.DateOfBirth <= DateTimeOffset.Now + TimeSpan.FromDays(birthdayDiscount.DateRangeInDays); return(birthdayDiscount.IsActive && DateOfBirthIsDiscountRange()); }
internal OverlapppedDiscounts(int itemGroupIndex, DiscountBase discount) { this.OverlapId = Guid.NewGuid(); this.MixAndMatchAndQuantityDiscounts = new Dictionary <string, DiscountBase>(StringComparer.OrdinalIgnoreCase); this.MixAndMatchAndQuantityDiscounts.Add(discount.OfferId, discount); this.OfferDiscounts = new Dictionary <string, DiscountBase>(StringComparer.OrdinalIgnoreCase); this.CoveredItemGroupIndexSet = new HashSet <int>() { itemGroupIndex }; }
private IActionResult AddDiscountToProduct(Guid productId, DiscountBase discount) { var product = unitOfWork.ProductRepository.GetProductById(productId); if (product == null) { return(BadRequest()); } product.Discounts.Add(discount); unitOfWork.ProductRepository.UpdateProduct(product); return(Ok()); }
/// <summary> /// Refreshes item compounded quantity lookup after each round of calculating discounts /// of exclusive or non-exclusive least-expensive-favor-retail discounts, of the same priority number. /// Prepares remaining quantities for the next round. /// </summary> /// <param name="bestDiscountPath">Best discount path of application discount applications so far.</param> /// <param name="lastPriorityNumber">Priority number of the last round.</param> /// <param name="remainingQuantities">Remaining quantities for the next round.</param> /// <param name="remainingQuantitiesForCompound">Remaining quantities for compound for the next round.</param> internal void RefreshLookupAndPrepareRemainingQuantitiesForLeastExpensiveFavoRetailer( List <AppliedDiscountApplication> bestDiscountPath, int lastPriorityNumber, decimal[] remainingQuantities, decimal[] remainingQuantitiesForCompound) { // Step 1: fill in this.ItemGroupIndexToCompoundedOfferToQuantityLookup from the last round. // reduce remaining quantitied for non-compounded discounts. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); foreach (AppliedDiscountApplication appliedDiscountApplication in bestDiscountPath) { DiscountBase discount = appliedDiscountApplication.DiscountApplication.Discount; if (discount.CanCompound) { // We've already processed the lookups for the previous round. Here we only processed the most recent round. // Only mix and match least expensive favoring retailer MixAndMatchDiscount mixAndMatch = discount as MixAndMatchDiscount; if (discount.PricingPriorityNumber == lastPriorityNumber && (mixAndMatch != null && mixAndMatch.DiscountType == DiscountMethodType.LeastExpensive && mixAndMatch.LeastExpensiveMode == LeastExpensiveMode.FavorRetailer)) { this.FillLookup(appliedDiscountApplication); } } else { ReduceQuantitiesFromNonCompounded(appliedDiscountApplication, remainingQuantities, remainingQuantitiesForCompound); } } // Step 2: move the data to settled lookups. foreach (KeyValuePair <int, Dictionary <string, decimal> > pair in this.ItemGroupIndexToCompoundedOfferToQuantityLookup) { int itemGroupIndex = pair.Key; Dictionary <string, decimal> offerIdToQuantityLookup = pair.Value; decimal compoundedQuantity = offerIdToQuantityLookup.Max(p => p.Value); AddToOrUpdateItemGroupIndexToPriorityToSettledCompoundedQuantityLookup( itemGroupIndex, lastPriorityNumber, compoundedQuantity, this.itemGroupIndexToPriorityToSettledCompoundedQuantityForNonThreshold); } this.ReduceQuantitiesFromCompoundedForNonThresholds( remainingQuantities, remainingQuantitiesForCompound, null); // Clear current lookup. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); }
/// <summary> /// Refreshes item compounded quantity lookup after each round of calculating discounts /// of exclusive or non-exclusive non-thresholds discounts, of the same priority number. /// Prepares remaining quantities for the next round. /// </summary> /// <param name="bestDiscountPath">Best discount path of application discount applications so far.</param> /// <param name="lastPriorityNumber">Priority number of the last round.</param> /// <param name="remainingQuantities">Remaining quantities for the next round.</param> /// <param name="remainingQuantitiesForCompound">Remaining quantities for compound for the next round.</param> /// <param name="reserveCompoundedQuantityForLeastExpensiveFavorRetailer"> /// If it's followed by least expensive favoring retail of the same priority, then reserve the compounded quantity of the same priority. /// </param> internal void RefreshLookupAndPrepareRemainingQuantitiesForNonThresholds( List <AppliedDiscountApplication> bestDiscountPath, int lastPriorityNumber, decimal[] remainingQuantities, decimal[] remainingQuantitiesForCompound, bool reserveCompoundedQuantityForLeastExpensiveFavorRetailer) { // Step 1: fill in this.ItemGroupIndexToCompoundedOfferToQuantityLookup from the last round. // reduce remaining quantitied for non-compounded discounts. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); foreach (AppliedDiscountApplication appliedDiscountApplication in bestDiscountPath) { DiscountBase discount = appliedDiscountApplication.DiscountApplication.Discount; if (discount.CanCompound) { // We've already processed the lookups for the previous round. Here we only processed the most recent round. // Ignore mix and match least expensive favoring retailer MixAndMatchDiscount mixAndMatch = discount as MixAndMatchDiscount; if (discount.PricingPriorityNumber == lastPriorityNumber && discount.PeriodicDiscountType != PeriodicDiscountOfferType.Threshold && (mixAndMatch == null || mixAndMatch.DiscountType != DiscountMethodType.LeastExpensive || mixAndMatch.LeastExpensiveMode == LeastExpensiveMode.FavorCustomer)) { this.FillLookup(appliedDiscountApplication); } } else { ReduceQuantitiesFromNonCompounded(appliedDiscountApplication, remainingQuantities, remainingQuantitiesForCompound); } } // Step 2: move the data to settled lookups. this.MoveToSettledCompoundedQuantityLookup(lastPriorityNumber, this.itemGroupIndexToPriorityToSettledCompoundedQuantityForNonThreshold); int?priorityNumberToSkipForCompounded = null; if (reserveCompoundedQuantityForLeastExpensiveFavorRetailer) { priorityNumberToSkipForCompounded = lastPriorityNumber; } this.ReduceQuantitiesFromCompoundedForNonThresholds( remainingQuantities, remainingQuantitiesForCompound, priorityNumberToSkipForCompounded); // Clear current lookup. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); }
public override DiscountBase GetDiscountStrategy(Basket basket, DiscountBase parentDiscountStrategy) { DiscountBase bestStrategy = null; double bestPrice = 0; foreach (KeyValuePair <DiscountTypesEnum, Func <DiscountBase, DiscountBase> > discountStrategy in BasketDiscountOverMemberShipFactory.Creators) { DiscountBase currentStrategy = discountStrategy.Value(parentDiscountStrategy); if (currentStrategy.IsEnabled(basket) && (bestStrategy == null || bestStrategy.GetDiscountedTotal(basket) > currentStrategy.GetDiscountedTotal(basket))) { (bestPrice, bestStrategy) = this.GetBetterStrategy(bestPrice, bestStrategy, basket, currentStrategy); } } return(bestStrategy); }
public void BasketDiscount15PercentTest() { //Arrange User u = new User("user", MemberShipTypeEnum.STANDART); Basket basket = new Basket(u, DateTime.Now); Product p1 = new Product("p1", 100, 90); Product p2 = new Product("p2", 200, 180); basket.AddOrderProduct(new BasketProduct(p1, 1)); basket.AddOrderProduct(new BasketProduct(p2, 1)); //Act DiscountBase discountStrategy = StandartDiscountFactory.Instance.GetDiscountStrategy(basket); double discountedPrice = basket.calcDiscountedTotal(discountStrategy); //Assert discountedPrice.Should().Be(255); }
public void BlackFriday40PercentTest() { //Arrange User u = new User("user", MemberShipTypeEnum.STANDART); Basket basket = new Basket(u, new DateTime(2019, 11, 29)); Product p1 = new Product("p1", 700, 650); Product p2 = new Product("p2", 500, 400); basket.AddOrderProduct(new BasketProduct(p1, 1)); basket.AddOrderProduct(new BasketProduct(p2, 1)); //Act DiscountBase discountStrategy = StandartDiscountFactory.Instance.GetDiscountStrategy(basket); double discountedPrice = basket.calcDiscountedTotal(discountStrategy); //Assert discountedPrice.Should().Be(720); }
private static DiscountDealEstimate BuildEstimates( Dictionary <string, DiscountDealEstimate> offerIdToEstimateNonCompoundedLookupHolder, List <DiscountBase> compoundedDiscountsHolder, DiscountDealEstimate existingCombinedEstimatesForCompounded, Dictionary <string, DiscountBase> discounts, DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities, decimal[] remainingQuantitiesForCompound, HashSet <int> itemsWithOverlappingDiscounts, HashSet <int> itemsWithOverlappingDiscountsCompoundedOnly) { using (SimpleProfiler profiler = new SimpleProfiler("OverlappedDiscounts.BuildEstimates", 2)) { DiscountDealEstimate combinedEstimateForCompounded = existingCombinedEstimatesForCompounded; foreach (KeyValuePair <string, DiscountBase> pair in discounts) { DiscountBase discount = pair.Value; DiscountDealEstimate estimate = discount.GetDiscountDealEstimate( discountableItemGroups, discount.CanCompound ? remainingQuantitiesForCompound : remainingQuantities, itemsWithOverlappingDiscounts, itemsWithOverlappingDiscountsCompoundedOnly); if (discount.CanCompound) { if (combinedEstimateForCompounded == null) { combinedEstimateForCompounded = estimate; } else { combinedEstimateForCompounded = DiscountDealEstimate.Combine(combinedEstimateForCompounded, estimate); } compoundedDiscountsHolder.Add(discount); } else { offerIdToEstimateNonCompoundedLookupHolder[discount.OfferId] = estimate; } } // returns combined estimate for compounded return(combinedEstimateForCompounded); } }
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); }
private void Aquire( Dictionary <int, OverlapppedDiscounts> itemGroupIndexToOverlapppedDiscountsLookup, OverlapppedDiscounts overlapppedDiscountsToAcquire) { using (SimpleProfiler profiler = new SimpleProfiler("OverlappedDiscounts.Aquire", 4)) { this.MixAndMatchAndQuantityDiscounts.AddRange(overlapppedDiscountsToAcquire.MixAndMatchAndQuantityDiscounts); this.CoveredItemGroupIndexSet.AddRange(overlapppedDiscountsToAcquire.CoveredItemGroupIndexSet); foreach (KeyValuePair <string, DiscountBase> pairOfferIdToDiscount in overlapppedDiscountsToAcquire.MixAndMatchAndQuantityDiscounts) { DiscountBase discount = pairOfferIdToDiscount.Value; foreach (KeyValuePair <int, HashSet <decimal> > pair in discount.ItemGroupIndexToDiscountLineNumberSetMap) { int itemGroupIndex = pair.Key; itemGroupIndexToOverlapppedDiscountsLookup[itemGroupIndex] = this; } } } }
/// <summary> /// Refreshes item compounded quantity lookup after each round of calculating discounts /// of exclusive or non-exclusive thresholds discounts, of the same priority number. /// Prepares remaining quantities for the next round. /// </summary> /// <param name="bestDiscountPath">Best discount path of application discount applications so far.</param> /// <param name="lastPriorityNumber">Priority number of the last round.</param> /// <param name="remainingQuantities">Remaining quantities for the next round.</param> /// <param name="remainingQuantitiesForCompound">Remaining quantities for compound for the next round.</param> internal void RefreshLookupAndPrepareRemainingQuantitiesForThresholds( List <AppliedDiscountApplication> bestDiscountPath, int lastPriorityNumber, decimal[] remainingQuantities, decimal[] remainingQuantitiesForCompound) { // Step 1: fill in this.ItemGroupIndexToCompoundedOfferToQuantityLookup from the last round. // reduce remaining quantitied for non-compounded discounts. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); foreach (AppliedDiscountApplication appliedDiscountApplication in bestDiscountPath) { DiscountBase discount = appliedDiscountApplication.DiscountApplication.Discount; if (discount.CanCompound) { // We've already processed the lookups for the previous round. Here we only processed the most recent round. if (discount.PricingPriorityNumber == lastPriorityNumber && discount.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold) { this.FillLookup(appliedDiscountApplication); } } else { ReduceQuantitiesFromNonCompounded(appliedDiscountApplication, remainingQuantities, remainingQuantitiesForCompound); } } // Step 2: move the data to settled lookups. this.MoveToSettledCompoundedQuantityLookup(lastPriorityNumber, this.itemGroupIndexToPriorityToSettledCompoundedQuantityForThreshold); // Step 3: figure out remain quantities for threshold evaluation. this.ReduceQuantitiesFromCompoundedThresholdsForThresholds(remainingQuantities, remainingQuantitiesForCompound); this.ReduceQuantitiesFromCompoundedNonThresholdForThreshold(remainingQuantities); // Clear current lookup. this.ItemGroupIndexToCompoundedOfferToQuantityLookup.Clear(); }
private void AddDiscount(DiscountBase discount) { if (discount is AmountDiscount) { AmountDiscount a = discount as AmountDiscount; AmountDiscount aad = GetAmountDiscount(discount.DiscountType); aad.DiscountAmount += a.DiscountAmount; } if (discount is ProductAsDiscount) { ProductAsDiscount a = discount as ProductAsDiscount; ProductAsDiscount pad = GetProductAsDiscount(discount.DiscountType,a.ProductId); pad.Quantity += a.Quantity; } }
internal bool IsOkayForMixAndMatchLeastExpensiveOneLineGroupPartialOptimizationAndFillupValueLookups( Dictionary <int, decimal> mixAndMatchRelativeValueLookup, List <int> itemGroupIndexListSortedByRelativePriceDescending, List <HashSet <int> > consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending, Dictionary <int, OfferDiscount> simpleDiscountOfferLookup, DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities) { // Special optimization for this overlapped group when there is // only one mix and match // with only one line group // with least expensive deal price and %-off bool isOkay = this.MixAndMatchAndQuantityDiscounts.Count == 1; consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending.Clear(); MixAndMatchDiscount mixAndMatchDiscount = null; if (isOkay) { mixAndMatchDiscount = this.MixAndMatchAndQuantityDiscounts.First().Value as MixAndMatchDiscount; isOkay = mixAndMatchDiscount != null; } if (isOkay) { isOkay = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.Count == 1; } if (isOkay) { isOkay = mixAndMatchDiscount.DiscountType == DiscountMethodType.LeastExpensive && !mixAndMatchDiscount.IsLeastExpensiveAmountOff; } if (isOkay) { var lineGroupNumberOfItemsPair = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.First(); string lineGroup = lineGroupNumberOfItemsPair.Key; decimal quantityNeeded = lineGroupNumberOfItemsPair.Value; HashSet <int> mixAndMatchItemIndexGroupSet = null; if (mixAndMatchDiscount.LineGroupToItemGroupIndexSetLookup.TryGetValue(lineGroup, out mixAndMatchItemIndexGroupSet)) { foreach (int itemGroupIndex in mixAndMatchItemIndexGroupSet) { if (remainingQuantities[itemGroupIndex] > decimal.Zero) { // Move it to mix and match decimal mixAndMatchValue = decimal.Zero; decimal price = discountableItemGroups[itemGroupIndex].Price; decimal totalPriceForDiscount = price * mixAndMatchDiscount.NumberOfLeastExpensiveLines; if (mixAndMatchDiscount.DiscountPercentValue > decimal.Zero) { mixAndMatchValue = DiscountBase.GetDiscountAmountForPercentageOff(totalPriceForDiscount, mixAndMatchDiscount.DiscountPercentValue); } else if (mixAndMatchDiscount.DiscountAmountValue == decimal.Zero && mixAndMatchDiscount.DealPriceValue > decimal.Zero) { mixAndMatchValue = totalPriceForDiscount - mixAndMatchDiscount.DealPriceValue; if (mixAndMatchValue < decimal.Zero) { isOkay = false; } } if (!isOkay) { break; } decimal simpleDiscountValue = decimal.Zero; int discountOfferCount = 0; // If an item has compounded discounts only, then we have already processed compounded discount offers, so we won't see them here. // And this won't affect algorithm. // If we modify optimization before overlapped discounts, then this may not be true. // See DiscountCalcuator.ReduceOverlappedOfferAndQuantityDiscountsPerItem. foreach (KeyValuePair <string, DiscountBase> pair in this.OfferDiscounts) { OfferDiscount offer = pair.Value as OfferDiscount; if (offer != null) { HashSet <decimal> discountLineNumberSetForItem = null; if (offer.ItemGroupIndexToDiscountLineNumberSetMap.TryGetValue(itemGroupIndex, out discountLineNumberSetForItem)) { if (discountLineNumberSetForItem.Count >= 1) { decimal discountLineNumber = discountLineNumberSetForItem.First(); RetailDiscountLine discountLineDefinition = offer.DiscountLines[discountLineNumber]; // For now, it works for fully covered items. // Fully covered, as an example, buy 3 for $10, then if item price is $3, it won't get discount // or buy 3 for $5 off, then if item price is $1, it won't get full discount. bool isFullyCovered = false; simpleDiscountValue = OfferDiscount.GetUnitDiscountAmountAndCheckWhetherItsFullyCovered(discountLineDefinition, price, out isFullyCovered) * quantityNeeded; if (!isFullyCovered) { isOkay = false; } if (!isOkay) { break; } simpleDiscountOfferLookup[itemGroupIndex] = offer; discountOfferCount++; } } } } // For now, we don't handle multiple discount offers. // With optimization earlier, that's mostly the case. See DiscountCalcuator.ReduceOverlappedOfferAndQuantityDiscountsPerItem. if (discountOfferCount > 1) { isOkay = false; } if (!isOkay) { break; } mixAndMatchRelativeValueLookup.Add(itemGroupIndex, mixAndMatchValue - simpleDiscountValue); } } } } if (isOkay) { // Now group items by relative price and price, and order them by relative price, in consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending. // See tests in OverlappedDiscountsUnitTests. itemGroupIndexListSortedByRelativePriceDescending.AddRange(mixAndMatchRelativeValueLookup.Keys); ItemPriceComparer itemPriceComparer = new ItemPriceComparer(mixAndMatchRelativeValueLookup); itemGroupIndexListSortedByRelativePriceDescending.Sort(itemPriceComparer.GetComparison()); decimal previousRelativePrice = decimal.Zero; Dictionary <decimal, HashSet <int> > itemPriceToItemGroupIndexSetLookup = new Dictionary <decimal, HashSet <int> >(); for (int listIndex = 0; listIndex < itemGroupIndexListSortedByRelativePriceDescending.Count; listIndex++) { int itemGroupIndex = itemGroupIndexListSortedByRelativePriceDescending[listIndex]; decimal currentPrice = discountableItemGroups[itemGroupIndex].Price; decimal currentRelativePrice = mixAndMatchRelativeValueLookup[itemGroupIndex]; if (listIndex == 0) { previousRelativePrice = currentRelativePrice; itemPriceToItemGroupIndexSetLookup.Add(currentPrice, new HashSet <int>() { itemGroupIndex }); } else { if (currentRelativePrice == previousRelativePrice) { HashSet <int> itemGroupIndexSetWithSameRelativePriceAndPrice = null; if (itemPriceToItemGroupIndexSetLookup.TryGetValue(currentPrice, out itemGroupIndexSetWithSameRelativePriceAndPrice)) { itemGroupIndexSetWithSameRelativePriceAndPrice.Add(itemGroupIndex); } else { itemPriceToItemGroupIndexSetLookup.Add(currentPrice, new HashSet <int>() { itemGroupIndex }); } } if (currentRelativePrice != previousRelativePrice) { foreach (KeyValuePair <decimal, HashSet <int> > pair in itemPriceToItemGroupIndexSetLookup) { HashSet <int> itemGroupIndexSetWithSameRelativePriceAndPrice = pair.Value; consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending.Add(new HashSet <int>(itemGroupIndexSetWithSameRelativePriceAndPrice)); } previousRelativePrice = currentRelativePrice; itemPriceToItemGroupIndexSetLookup.Clear(); itemPriceToItemGroupIndexSetLookup.Add(currentPrice, new HashSet <int>() { itemGroupIndex }); } if (listIndex == itemGroupIndexListSortedByRelativePriceDescending.Count - 1) { foreach (KeyValuePair <decimal, HashSet <int> > pair in itemPriceToItemGroupIndexSetLookup) { HashSet <int> itemGroupIndexSetWithSameRelativePriceAndPrice = pair.Value; consolidatedListOfItemsWithSamePriceAndRelativeValuesDescending.Add(new HashSet <int>(itemGroupIndexSetWithSameRelativePriceAndPrice)); } itemPriceToItemGroupIndexSetLookup.Clear(); } } } } return(isOkay); }
internal DiscountBase[] GetSortedDiscountsToApplyInFastMode( DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities, decimal[] remainingQuantitiesForCompound, HashSet <int> itemsWithOverlappingDiscounts, HashSet <int> itemsWithOverlappingDiscountsCompoundedOnly) { using (SimpleProfiler profiler = new SimpleProfiler("OverlappedDiscounts.GetDiscountsToApplyInFastMode", 2)) { Dictionary <string, DiscountDealEstimate> offerIdToEstimateNonCompoundedLookup = new Dictionary <string, DiscountDealEstimate>(StringComparer.OrdinalIgnoreCase); // Consolidate all compounded discounts into one estimate, to be sorted with the rest later. List <DiscountBase> compoundedDiscounts = new List <DiscountBase>(); DiscountDealEstimate combinedEstimateForCompounded = null; // Build estimates for offer discounts. combinedEstimateForCompounded = OverlapppedDiscounts.BuildEstimates( offerIdToEstimateNonCompoundedLookup, compoundedDiscounts, combinedEstimateForCompounded, this.OfferDiscounts, discountableItemGroups, remainingQuantities, remainingQuantitiesForCompound, itemsWithOverlappingDiscounts, itemsWithOverlappingDiscountsCompoundedOnly); // Build estimates for mix and match and quantity discounts. combinedEstimateForCompounded = OverlapppedDiscounts.BuildEstimates( offerIdToEstimateNonCompoundedLookup, compoundedDiscounts, combinedEstimateForCompounded, this.MixAndMatchAndQuantityDiscounts, discountableItemGroups, remainingQuantities, remainingQuantitiesForCompound, itemsWithOverlappingDiscounts, itemsWithOverlappingDiscountsCompoundedOnly); List <DiscountDealEstimate> estimatedSorted = new List <DiscountDealEstimate>(offerIdToEstimateNonCompoundedLookup.Values); if (combinedEstimateForCompounded != null) { estimatedSorted.Add(combinedEstimateForCompounded); } estimatedSorted.Sort(DiscountDealEstimate.GetComparison()); #if DEBUG foreach (DiscountDealEstimate estimate in estimatedSorted) { estimate.DebugDisplay(); } #endif DiscountBase[] discountsSorted = new DiscountBase[this.MixAndMatchAndQuantityDiscounts.Count + this.OfferDiscounts.Count]; int discountIndex = 0; for (int i = estimatedSorted.Count - 1; i >= 0; i--) { DiscountDealEstimate estimate = estimatedSorted[i]; if (estimate.CanCompound) { for (int compoundedIndex = 0; compoundedIndex < compoundedDiscounts.Count; compoundedIndex++) { discountsSorted[discountIndex] = compoundedDiscounts[compoundedIndex]; discountIndex++; } } else { DiscountBase discount = null; if (this.MixAndMatchAndQuantityDiscounts.TryGetValue(estimate.OfferId, out discount)) { discountsSorted[discountIndex] = discount; discountIndex++; } else if (this.OfferDiscounts.TryGetValue(estimate.OfferId, out discount)) { discountsSorted[discountIndex] = discount; discountIndex++; } } } return(discountsSorted); } }
internal bool IsOkayForMixAndMatchOneLineGroupOptimizationAndFillupValueLookups( Dictionary <int, decimal> mixAndMatchRelativeValueLookup, Dictionary <int, OfferDiscount> simpleDiscountOfferLookup, DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities) { // Special optimization for this overlapped group when there is // only one mix and match // with only one line group // with any of deal price, $ off, % off, or least expensive $-off. bool isOkay = this.MixAndMatchAndQuantityDiscounts.Count == 1; MixAndMatchDiscount mixAndMatchDiscount = null; if (isOkay) { mixAndMatchDiscount = this.MixAndMatchAndQuantityDiscounts.First().Value as MixAndMatchDiscount; isOkay = mixAndMatchDiscount != null; } if (isOkay) { isOkay = mixAndMatchDiscount.DiscountType == DiscountMethodType.DealPrice || mixAndMatchDiscount.DiscountType == DiscountMethodType.DiscountPercent || mixAndMatchDiscount.DiscountType == DiscountMethodType.DiscountAmount || mixAndMatchDiscount.IsLeastExpensiveAmountOff; } if (isOkay) { isOkay = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.Count == 1; } if (isOkay) { // Mix and match example: buy 3 for $10. // For each item, calculate relative value of mix and match against discount offer with quantity 3 as a group. // relative value of item A = M(AAA) - 3S(A) // See DiscountCalculator.TryOptimizeForOneMixAndMatchWithOneLineGroup var lineGroupNumberOfItemsPair = mixAndMatchDiscount.LineGroupToNumberOfItemsMap.First(); string lineGroup = lineGroupNumberOfItemsPair.Key; decimal quantityNeeded = lineGroupNumberOfItemsPair.Value; HashSet <int> mixAndMatchItemIndexGroupSet = null; if (mixAndMatchDiscount.LineGroupToItemGroupIndexSetLookup.TryGetValue(lineGroup, out mixAndMatchItemIndexGroupSet)) { foreach (int itemGroupIndex in mixAndMatchItemIndexGroupSet) { if (remainingQuantities[itemGroupIndex] > decimal.Zero) { // Move it to mix and match decimal mixAndMatchValue = decimal.Zero; decimal price = discountableItemGroups[itemGroupIndex].Price; decimal totalPrice = price * quantityNeeded; switch (mixAndMatchDiscount.DiscountType) { case DiscountMethodType.DealPrice: mixAndMatchValue = totalPrice - mixAndMatchDiscount.DealPriceValue; isOkay = mixAndMatchValue >= decimal.Zero; break; case DiscountMethodType.DiscountAmount: mixAndMatchValue = mixAndMatchDiscount.DiscountAmountValue; isOkay = totalPrice >= mixAndMatchDiscount.DiscountAmountValue; break; case DiscountMethodType.DiscountPercent: mixAndMatchValue = DiscountBase.GetDiscountAmountForPercentageOff(totalPrice, mixAndMatchDiscount.DiscountPercentValue); break; default: isOkay = false; if (mixAndMatchDiscount.IsLeastExpensiveAmountOff && price >= mixAndMatchDiscount.DiscountAmountValue) { isOkay = true; mixAndMatchValue = mixAndMatchDiscount.DiscountAmountValue; } break; } if (!isOkay) { break; } int discountOfferCount = 0; decimal simpleDiscountValue = decimal.Zero; foreach (KeyValuePair <string, DiscountBase> pair in this.OfferDiscounts) { OfferDiscount offer = pair.Value as OfferDiscount; if (offer != null) { HashSet <decimal> discountLineNumberSetForItem = null; if (offer.ItemGroupIndexToDiscountLineNumberSetMap.TryGetValue(itemGroupIndex, out discountLineNumberSetForItem)) { if (discountLineNumberSetForItem.Count >= 1) { decimal discountLineNumber = discountLineNumberSetForItem.First(); RetailDiscountLine discountLineDefinition = offer.DiscountLines[discountLineNumber]; // For now, it works for fully covered items. // Fully covered, as an example, buy 3 for $10, then if item price is $3, it won't get discount // or buy 3 for $5 off, then if item price is $1, it won't get full discount. bool isFullyCovered = false; simpleDiscountValue = OfferDiscount.GetUnitDiscountAmountAndCheckWhetherItsFullyCovered(discountLineDefinition, price, out isFullyCovered) * quantityNeeded; if (!isFullyCovered) { isOkay = false; } if (!isOkay) { break; } simpleDiscountOfferLookup[itemGroupIndex] = offer; discountOfferCount++; } } } } // For now, we don't handle multiple discount offers. // With optimization earlier, that's mostly the case. See DiscountCalcuator.ReduceOverlappedOfferAndQuantityDiscountsPerItem. if (discountOfferCount > 1) { isOkay = false; } if (!isOkay) { break; } mixAndMatchRelativeValueLookup.Add(itemGroupIndex, mixAndMatchValue - simpleDiscountValue); } } } } return(isOkay); }
protected MovieLicenseBase(string movie, DateTime purchaseTime, DiscountBase discount) { Movie = movie; PurchaseTime = purchaseTime; _discount = discount; }
public double calcDiscountedTotal(DiscountBase discountStrategy) { DiscountedTotal = discountStrategy.GetDiscountedTotal(this); return(DiscountedTotal); }
public abstract DiscountBase GetDiscountStrategy(Basket basket, DiscountBase parentDiscountStrategy);
public TwoDaysMovieLicense(string movie, DateTime purchaseTime, DiscountBase discount) : base(movie, purchaseTime, discount) { }
protected virtual (double price, DiscountBase) GetBetterStrategy(double discountedPrice, DiscountBase currentStragey, Basket basket, DiscountBase nextDiscountStrategy) { if (nextDiscountStrategy != null) { double nextPrice = nextDiscountStrategy.GetDiscountedTotal(basket); if (currentStragey == null || nextPrice < discountedPrice) { return(nextPrice, nextDiscountStrategy); } } return(discountedPrice, currentStragey); }
public override AppliedDiscountApplication GetAppliedDiscountApplication( DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities, IEnumerable <AppliedDiscountApplication> appliedDiscounts, DiscountApplication discountApplication, PriceContext priceContext) { if (discountApplication == null || !discountApplication.RetailDiscountLines.Any() || discountableItemGroups == null || remainingQuantities == null) { return(null); } decimal[] prices = new decimal[discountableItemGroups.Length]; Dictionary <int, IList <DiscountLineQuantity> > discountDictionary = this.GetExistingDiscountDictionaryAndDiscountedPrices( discountableItemGroups, remainingQuantities, appliedDiscounts, discountApplication, true, true, prices); RetailDiscountLineItem retailDiscountLineItem = discountApplication.RetailDiscountLines.ElementAt(0); DiscountOfferMethod discountMethod = (DiscountOfferMethod)retailDiscountLineItem.RetailDiscountLine.DiscountMethod; decimal dealPrice = decimal.Zero; decimal discountValue = decimal.Zero; decimal discountAmountForDiscountLine = decimal.Zero; switch (discountMethod) { case DiscountOfferMethod.DiscountAmount: discountValue = retailDiscountLineItem.RetailDiscountLine.DiscountAmount; discountAmountForDiscountLine = discountValue; break; case DiscountOfferMethod.DiscountPercent: discountValue = prices[retailDiscountLineItem.ItemIndex] * (retailDiscountLineItem.RetailDiscountLine.DiscountPercent / 100M); break; case DiscountOfferMethod.OfferPrice: dealPrice = retailDiscountLineItem.RetailDiscountLine.OfferPrice; decimal bestExistingDealPrice = 0m; bool hasExistingDealPrice = DiscountBase.TryGetBestExistingDealPrice(discountDictionary, retailDiscountLineItem.ItemIndex, out bestExistingDealPrice); // We don't use discounted price here. discountValue = DiscountBase.GetDiscountAmountFromDealPrice(discountableItemGroups[retailDiscountLineItem.ItemIndex].Price, hasExistingDealPrice, bestExistingDealPrice, dealPrice); discountAmountForDiscountLine = discountValue; break; default: break; } // When has no competing discounts or compounded, apply all remaining quantity. bool applyAllAvailableQuantity = (discountApplication.ApplyStandalone || this.CanCompound) && !discountApplication.HonorQuantity; decimal quantityToApply = applyAllAvailableQuantity ? remainingQuantities[retailDiscountLineItem.ItemIndex] : discountApplication.ItemQuantities[retailDiscountLineItem.ItemIndex]; decimal result = discountValue * quantityToApply; AppliedDiscountApplication newAppliedDiscountApplication = null; if (result > decimal.Zero) { Dictionary <int, decimal> itemQuantities; if (applyAllAvailableQuantity) { itemQuantities = new Dictionary <int, decimal>(); itemQuantities[retailDiscountLineItem.ItemIndex] = quantityToApply; } else { itemQuantities = discountApplication.ItemQuantities; } newAppliedDiscountApplication = new AppliedDiscountApplication(discountApplication, result, itemQuantities, isDiscountLineGenerated: true); DiscountLine discountLine = this.NewDiscountLine(discountApplication.DiscountCode, discountableItemGroups[retailDiscountLineItem.ItemIndex].ItemId); discountLine.PeriodicDiscountType = PeriodicDiscountOfferType.Offer; discountLine.DealPrice = dealPrice; discountLine.Amount = discountAmountForDiscountLine; discountLine.Percentage = retailDiscountLineItem.RetailDiscountLine.DiscountPercent; newAppliedDiscountApplication.AddDiscountLine(retailDiscountLineItem.ItemIndex, new DiscountLineQuantity(discountLine, itemQuantities[retailDiscountLineItem.ItemIndex])); if (discountApplication.RemoveItemsFromLookupsWhenApplied) { this.RemoveItemIndexGroupFromLookups(retailDiscountLineItem.ItemIndex); } } return(newAppliedDiscountApplication); }