Try our conversational search powered by Generative AI!

Sanjay Kumar
Jan 3, 2023
  1260
(7 votes)

Custom promotion - Buy at least X items from catalog entries and get a discount on related catalog entries.

Problem: Buy at least X items from catalog entries and get related catalog entries at a discount that satisfy the below formula.

 or

Create a custom promotion to ‘Buy Products for Discount from Other Selections’ and apply the below formula to get a discount on other selections.

Formula = m x n,  then get the discounts on n items.

m = Spend at least X items

n = Multiplier

e.g.

m = 2

n = 1, 2,3……

CART
Main Product SKU Qty. Other Selection SKU Qty. Eligible Discount Qty.
2 x 1 1 1
2 x 2 2 2
2 x 3 4 3
2 x 4 5 4
.... .... ....
m x n .... n

If you notice the above table ‘Other Selection SKU Qty.’ column, customer added more than the eligible discounted qty in the cart. So, we need to make sure the discount is only eligible for 'Eligible Discount Qty.' not for all ‘Other Selection SKU Qty.’.

Solution:

  1. Create a custom entry promotion
   [ContentType(
        GUID = "CB99C622-A170-4EF6-B28D-077B18BAFD81",
        GroupName = "Custom - Promotions",
        DisplayName = "Custom - Buy products for discount from other selection",
        Description = "Buy at least X items from catalog entries and get related catalog entries at a discount e.g. (2 + 1),  (4 + 2),  (6 + 3) ... ",
        Order = 30000)]
    [PromotionSettings(FixedRedemptionsPerOrder = 1)]
    [ImageUrl("~/Static/Img/CustomBuyQuantityGetItemDiscount.png")]
    public class CustomBuyQuantityGetItemDiscount : EntryPromotion, IMonetaryDiscount
    {
        [Display(Order = 10)]
        [PromotionRegion("Condition")]
        public virtual PurchaseQuantity Condition { get; set; }

        [Display(Order = 20)]
        [PromotionRegion("Reward")]
        public virtual DiscountItems DiscountTarget { get; set; }

        [Display(Order = 30)]
        [PromotionRegion("Discount")]
        public virtual MonetaryReward Discount { get; set; }
    }

2. Create custom discount processor and override following methods:

  • GetPromotionItems
    • Return promotion conditions and reward items.
  • RewardDescription
    • Read all discounted SKUs with the max eligible qty for discounts.
    • Create Redemption Description into GetRedemptions.
public class CustomBuyQuantityGetItemDiscountProcessor : EntryPromotionProcessorBase<CustomBuyQuantityGetItemDiscount>
{
        private readonly IContentLoader _contentLoader;

        public CustomBuyQuantityGetItemDiscountProcessor(
            RedemptionDescriptionFactory redemptionDescriptionFactory,
            IContentLoader contentLoader)
            : base(redemptionDescriptionFactory)
        {
            _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
        }

        protected override PromotionItems GetPromotionItems(CustomBuyQuantityGetItemDiscount promotionData)
        {
            return new PromotionItems(
                promotionData,
                new CatalogItemSelection(promotionData.Condition.Items, CatalogItemSelectionType.Specific, true),
                new CatalogItemSelection(promotionData.DiscountTarget.Items, CatalogItemSelectionType.Specific, true));
        }

        protected override RewardDescription Evaluate(
            CustomBuyQuantityGetItemDiscount promotionData,
            PromotionProcessorContext context)
        {
            var allLineItems = this.GetLineItems(context.OrderForm)?
                .Where(x => !x.IsGift)?
                .ToList();

            if (promotionData?.DiscountTarget?.Items == null ||
                promotionData?.Condition?.Items == null ||
                promotionData?.Condition?.RequiredQuantity == 0 ||
                allLineItems.Count() == 0)
            {
                return
                    RewardDescription.CreateNotFulfilledDescription(
                    promotionData,
                    FulfillmentStatus.NotFulfilled);
            }

            //Read all excluded variants 
            var excludedVariants = new List<string>() { };
            if (promotionData.ExcludedItems != null && promotionData.ExcludedItems.Count > 0)
            {
                foreach (ContentReference item in promotionData.ExcludedItems)
                {
                    if (_contentLoader.TryGet(item, out NodeContent content))
                    {
                        excludedVariants.AddRange(
                            _contentLoader.GetDescendantsOfType<VariationContent>(content.ContentLink)
                            .Select(x => x.Code)
                            .ToList());
                    }
                    else if (_contentLoader.TryGet(item, out ProductContent productContent))
                    {
                        excludedVariants.AddRange(
                            _contentLoader.GetDescendantsOfType<VariationContent>(productContent.ContentLink)
                            .Select(x => x.Code)
                            .ToList());
                    }
                    else
                    {
                        excludedVariants.Add(_contentLoader.TryGet<VariationContent>(item)?.Code);
                    }
                }
            }
          
           //Read all condition variants
            var conditionSkus = new List<string>() { };
            foreach (ContentReference item in promotionData.Condition.Items)
            {
                if (_contentLoader.TryGet(item, out NodeContent content))
                {
                    conditionSkus.AddRange(
                        _contentLoader.GetDescendantsOfType<VariationContent>(content.ContentLink)
                        .Select(x => x.Code)
                        .ToList());
                }
                else if (_contentLoader.TryGet(item, out ProductContent productContent))
                {
                    conditionSkus.AddRange(
                        _contentLoader.GetDescendantsOfType<VariationContent>(productContent.ContentLink)
                        .Select(x => x.Code)
                        .ToList());
                }
                else
                {
                    conditionSkus.Add(_contentLoader.TryGet<VariationContent>(item)?.Code);
                }
            }
            //Read all discounted variants
            var discountSkus = new List<string>() { };
            foreach (ContentReference item in promotionData.DiscountTarget.Items)
            {
                if (_contentLoader.TryGet(item, out NodeContent content))
                {
                    discountSkus.AddRange(
                        _contentLoader.GetDescendantsOfType<VariationContent>(content.ContentLink)
                        .Select(x => x.Code)
                        .ToList());
                }
                else if (_contentLoader.TryGet(item, out ProductContent productContent))
                {
                    discountSkus.AddRange(
                        _contentLoader.GetDescendantsOfType<VariationContent>(productContent.ContentLink)
                        .Select(x => x.Code)
                        .ToList());
                }
                else
                {
                    discountSkus.Add(_contentLoader.TryGet<VariationContent>(item)?.Code);
                }
            }

            //remove the discounted SKUs if exist in the excluded list.
            if (excludedVariants.Count() > 0)
            {
                discountSkus = discountSkus.Where(x => !excludedVariants.Contains(x)).ToList();
                conditionSkus = conditionSkus.Where(x => !excludedVariants.Contains(x)).ToList();
            }

            var totalPurchasedQty = allLineItems.Where(x => conditionSkus.Contains(x.Code)).Sum(x => x.Quantity);
            var maxDiscountedQty = Math.Floor(totalPurchasedQty / promotionData.Condition.RequiredQuantity);
            if (promotionData.DiscountTarget.MaxQuantity != null &&
                maxDiscountedQty > promotionData.DiscountTarget.MaxQuantity)
            {
                maxDiscountedQty = (decimal)promotionData.DiscountTarget.MaxQuantity;
            }

            if (maxDiscountedQty == decimal.Zero)
            {
                return RewardDescription.CreateNotFulfilledDescription(
                       promotionData,
                       FulfillmentStatus.InvalidCoupon);
            }

            var discountedLineItems = allLineItems
                .Where(x => discountSkus.Contains(x.Code))
                .OrderByDescending(x => x.PlacedPrice)
                .ThenBy(x => x.Quantity)
                .ToList();

            return
                RewardDescription.CreateMoneyOrPercentageRewardDescription(
                         FulfillmentStatus.Fulfilled,
                         GetRedemptions(promotionData, context, discountedLineItems, maxDiscountedQty),
                         promotionData,
                         promotionData.Discount,
                         context.OrderGroup.Currency,
                         promotionData.Name);
        }

        private IEnumerable<RedemptionDescription> GetRedemptions(
          CustomBuyQuantityGetItemDiscount promotionData,
          PromotionProcessorContext context,
          List<ILineItem> discountedLineItems,
          decimal maxQty)
        {
            List<RedemptionDescription> redemptionDescriptionList = new List<RedemptionDescription>();
            var applicableCodes = discountedLineItems.Select(x => x.Code);
            decimal val2 = GetLineItems(context.OrderForm).Where(li => applicableCodes.Contains(li.Code)).Sum(li => li.Quantity);
            decimal num = Math.Min(GetMaxRedemptions(promotionData.RedemptionLimits), val2);

            for (int index = 0; index < num; ++index)
            {
                AffectedEntries entries = context.EntryPrices.ExtractEntries(applicableCodes, Math.Min(maxQty, val2), promotionData);
                if (entries != null)
                {
                    redemptionDescriptionList.Add(this.CreateRedemptionDescription(affectedEntries: entries));
                }
            }

            return redemptionDescriptionList;
        }
    }

Wishing you a year full of blessing and filled with a new adventure.

Happy new year 2023!

Cheers🥂

Jan 03, 2023

Comments

Ashish Rasal
Ashish Rasal Jan 6, 2023 03:57 AM

Nicely explained.

Please login to comment.
Latest blogs
The A/A Test: What You Need to Know

Sure, we all know what an A/B test can do. But what is an A/A test? How is it different? With an A/B test, we know that we can take a webpage (our...

Lindsey Rogers | Apr 15, 2024

.Net Core Timezone ID's Windows vs Linux

Hey all, First post here and I would like to talk about Timezone ID's and How Windows and Linux systems use different IDs. We currently run a .NET...

sheider | Apr 15, 2024

What's new in Language Manager 5.3.0

In Language Manager (LM) version 5.2.0, we added an option in appsettings.json called TranslateOrCopyContentAreaChildrenBlockForTypes . It does...

Quoc Anh Nguyen | Apr 15, 2024

Optimizely Search & Navigation: Boosting in Unified Search

In the Optimizely Search & Navigation admin view, administrators can set a certain weight of different properties (title, content, summary, or...

Tung Tran | Apr 15, 2024

Optimizely CMS – Getting all content of a specific property with a simple SQL script

When you need to retrieve all content of a specific property from a Page/Block type, normally you will use the IContentLoader or IContentRepository...

Tung Tran | Apr 15, 2024

Join the Content Recommendations Work Smarter webinar May 8th 16.00-16.45 CET with expert Aidan Swain

Learn more about Content Recommendations, with Optimizely’s very own Senior Solutions consultant, Aidan Swain . He will discuss best practices and...

Karen McDougall | Apr 12, 2024