SaaS CMS has officially launched! Learn more now.

Custom promotion applying different discounts per lineitem?

Vote:
 

I'm trying to understand if it's possible to create a custom promotion (https://docs.developers.optimizely.com/customized-commerce/docs/custom-promotions) in Optimizely Commerce (version 14.22.0) where certain line items get a percentage discount (15%) and certain other line items get a calculated discount based on the price of the line item?

The problem comes from having packages which contain a variant and the price of the package also includes labor cost for installing the variant, which should not be touched by the percentage discount. However, if the variant itself is added to the cart it should get a 15% discount straight up. 

Is this possible to handle in a way that some line items get 15% discount while for others we calculate the discount? Or is it possible to have a promotion where we calculate different discounts line item by line item?

I have to say that I find the documentation regarding custom promotions quite hard to follow so any help would be appreciated. 

#323808
Jun 19, 2024 12:47
Vote:
 

I have found a couple of questions in the forum with similar challenges, for instance:

But none of them give (not to me at least) a straightforward solution to the problem. I'm also wondering if this can be handled by creating our own version of ILineItemCalculator where labor cost is excluded from promotion calculations somehow but included in other calculations of line item prices? 

/Martin

#323811
Jun 19, 2024 13:53
Vote:
 

Hi Martin

It's very common to implement your own LineItemCalculator in commerce solution. I don't know much of your commerce modelling, at this stage I suggest you do a quick POC by creating your own LineItemCalculator by inherting from DefaultLineItemCalculator, and override "CalculateDiscountedPrice" to set labor cost lineitem discount to zero to see if it works as you expected. 

Line item calculator (optimizely.com)

#323848
Jun 20, 2024 1:11
Vote:
 

Hi Martin,

If you want to follow the approach that create a custom promotion for this one then I suggest that you can apply customization when returning reward description in Evaluate method of promotion processor like this

...
//Your business
IEnumerable<RedemptionDescription> redemptions = GetRedemptions(promotionData, context, applicableCodes);
//Add your business here to calculate discount
foreach (var redempt in redemptions)
{
    var manualSavedAmount = 0;
    foreach (var priceEntry in redempt.AffectedEntries.PriceEntries)
    {
        var discountAmount = 0;
        //TODO: Add your business here to calculate discountAmount of each price entry based on line item that you can get from priceEntry.ParentItem
        ...
        priceEntry.AdjustActualTotal(-discountAmount);
        manualSavedAmount += discountAmount;
    }
    redempt.SavedAmount = manualSavedAmount;
}
return new RewardDescription(fulfillmentStatus, redemptions, promotionData, 0m, 0m, RewardType.Money, fulfillmentStatus.GetRewardDescriptionText(_localizationService));

Hope that it works with you

#323851
Edited, Jun 20, 2024 7:22
Vote:
 

Hi Binh, 

Thank you so much for your response! Unfortunately the suggested solution does not seem to work, at least I can't see any discount being applied. Also there is no public setter available for SavedAmount so this line does not work: 

redempt.SavedAmount = manualSavedAmount;

Do you have a solution similar to this in any of your projects that works?

/Martin

#323856
Jun 20, 2024 11:19
Vote:
 

Hi Martin,

Actually I did something like this when I worked with EpiServer Commerce 13 and it worked. But SavedAmount is private property so I must use Reflection to update it value. I thought this prop is public in new version 14 but not. Just try to use Reflection for it. There is only 1 way that I can say if we want to do it in promotion processor.

        var savedAmountPropertyInfo = redempt.GetType().GetProperty("SavedAmount", BindingFlags.NonPublic | BindingFlags.Instance);

        if (savedAmountPropertyInfo != null)
        {
            savedAmountPropertyInfo .SetValue(redempt, manualSavedAmount);
        }
#323857
Jun 20, 2024 11:28
Vote:
 

Don't think that will work since this is what SavedAmount looks like in the RedemptionDescription class:

/// <summary>Gets the amount saved by the redeemed promotion.</summary>
    public Decimal SavedAmount
    {
      get
      {
        return this.AffectedObjects.Sum<IAffectedObject>((Func<IAffectedObject, Decimal>) (x => x.SavedAmount));
      }
    }

So even if it was possible to set the property using reflection I guess this code will still run when fetching the value for SavedAmount. 

#323858
Jun 20, 2024 11:50
Vote:
 

Seems to be possible to set SavedAmount on AffectedEntries like this:

((IAffectedObject)redempt.AffectedEntries).SavedAmount = manualSavedAmount;

but still I can't see any discount in the order management gui for instance. 

#323859
Jun 20, 2024 12:14
Vote:
 

Hi,

I may forgot some code that I did before.

Just try this one

...
//Your business
IEnumerable<RedemptionDescription> redemptions = GetRedemptions(promotionData, context, applicableCodes);
//Add your business here to calculate discount
foreach (var redempt in redemptions)
{
    var manualSavedAmount = 0;
    foreach (var priceEntry in redempt.AffectedEntries.PriceEntries)
    {
        var discountAmount = 0;
        //TODO: Add your business here to calculate discountAmount of each price entry based on line item that you can get from priceEntry.ParentItem
        ...
        priceEntry.Price = priceEntry.Price - discountAmount/priceEntry.Quantity;
        manualSavedAmount += discountAmount;
    }
    redempt.AffectedEntries.SavedAmount = manualSavedAmount;
}
return new RewardDescription(fulfillmentStatus, redemptions, promotionData, 0m, 0m, RewardType.Money, fulfillmentStatus.GetRewardDescriptionText(_localizationService));

Change to set price for price entry instead

#323860
Edited, Jun 20, 2024 12:32
Vote:
 

Hi Martin,

Does it work with you?

I corrected code a bit in my side and did testing in Foundation project and it works.

Here is the correct code

...
  var i = 1;

foreach (var redempt in redemptions)
  {
      var manualSavedAmount = 0m;
      foreach (var priceEntry in redempt.AffectedEntries.PriceEntries)
      {
         //Change by your business to calculate final price after discount
          priceEntry.Price = priceEntry.Price * i * 0.1m;
          manualSavedAmount += priceEntry.OriginalTotal - priceEntry.ActualTotal;

          i++;
      }
      ((IAffectedObject)redempt.AffectedEntries).SavedAmount = manualSavedAmount;
  }
  return new RewardDescription(fulfillmentStatus, redemptions, promotionData, 0m, 0m, RewardType.Money, fulfillmentStatus.GetRewardDescriptionText(_localizationService));

You can see result here

One item get discount 90% and one get discount 80% as my above example

#323901
Jun 21, 2024 10:42
Vote:
 

Sorry about late reply, friday was a holiday (midsummers eve) here in Sweden so didn't work that day. 

Yes that code actually seems to work quite ok, but I notice that for instance this list in Order management view for one of my carts show incorrect information under the header Discounts, do you see a similar thing in your Foundation-solution? Do you think there is a way to solve that? 

#324019
Jun 24, 2024 8:20
Vote:
 

Also the list of carts in Order management show a different total for the order than what shows when I open the cart. I'm just a bit worried that this might cause other side effects if the solution is not 100% working: 

#324023
Jun 24, 2024 8:34
Vote:
 

Hi Martin,

I saw same issue when testing in my side. Actually, there are some different logics between new version of commerce and old one version 13.x. 

But I found a way to do custom for new version.

  • Change reward type to RewardType.None like this
return new RewardDescription(reward.Status, reward.Redemptions, promotionData, 0m, 0m, RewardType.None, reward.Description);
  • Add custom implemetation for entry reward applicator for RewardType.None
 public class CustomEntryRewardApplicator : EntryRewardApplicator
 {
     protected override decimal ApplyNotSpecified(AffectedEntries item, RewardDescription reward, PromotionProcessorContext processorContext)
     {
         processorContext.EntryPrices.CommitPrices(item);

         return ((IAffectedObject)item).SavedAmount;
     }
 }
  • Register this new custom applicator in init module or start up
_services.AddSingleton<EntryRewardApplicator, CustomEntryRewardApplicator>();

Let's try to use it and do more test in your site to make sure it works completely.

#324034
Jun 24, 2024 15:14
Vote:
 

At a first glance that seems to work just fine, hopefully further testing will not show any more issues. 

Thanks a lot for your time and help!!

#324201
Jun 25, 2024 12:34
Binh Nguyen Thi - Jun 27, 2024 12:15
Nice to hear that!
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.