OrderCloud offers many examples of creating various promotions, including the newly added functionality that implements the buy-one, get-one-free promotion. However, it is not always possible to achieve the desired result using only built-in features. As powerful as the Rules Engine is, you may sometimes have a use case that is too complex to solve using the properties and functions accessible within an expression.
For this reason, OrderCloud has introduced a new feature, Promotion Integration, which allows you to apply and calculate custom discounts.
In this article, I would like to consider an example of the buy-one, get-one-free promotion. In this promotion, there is a certain category of products that can be purchased in pairs; that is, by buying two products, you get the second product from this category for free.
Creating a category
{ |
It is also necessary to create an assignment between this category, the buyer, and the user group.
{ |
Next, we need to create an assignment between the products and this category.
{ "CategoryID": "buy_one_get_one_free", "ProductID": "1234567890" } |
{ |
Creating a promotion integration
{ "HashKey": "YourHashKey", "Url": "your_promotionintegration_url" } |
Creating a promotion
{ "Name": "BuyOneGetOneFree", "Active": true, "Description": "Buy One Get One Free", "LineItemLevel": true, "Code": "anyCode", "StartDate": "2023-11-28T12:52:00+00:00", "ExpirationDate": "2030-12-30T12:52:00+00:00", "EligibleExpression": "item.Product.incategory('buy_one_get_one_free')", "ValueExpression": null, "CanCombine": true, "AllowAllBuyers": true, "AutoApply": true, "UseIntegration": true, "Priority": 0, "xp": { "Type": "BOGO" } } |
EligibleExpression says that this promotion will be applied to all the LineItems whose products are in this category.
The Value Expression is null, since UseIntegration is set to true, so we show that this promotion will use the promotion integration to calculate discounts.
In this case, an additional xp.Type property was added in order to differentiate the logic for different promotions using integration, if there are several of them.
Creating a controller and an action for promotion integration
The relevant implementation can be found in the GitHub repository:
mixershpixer/OrderCloud.PromotionIntegration.
[Route("promotionintegration")] public class PromotionIntegrationController : CatalystController { private readonly ICustomPromotionsCommand customPromotionsCommand; public PromotionIntegrationController( ICustomPromotionsCommand customPromotionsCommand) { this.customPromotionsCommand = customPromotionsCommand; } [HttpPost] public PromotionIntegrationResponse PromotionIntegration([FromBody] PromotionIntegrationPayload payload) { return customPromotionsCommand.CalculatePromotionIntegration(payload); } } |
According to the documentation, to use the promotion integration, we need a POST endpoint that includes the PromotionIntegrationPayload model.
public class PromotionIntegrationPayload |
And returns an object of the PromotionIntegrationResponse type.
public class PromotionIntegrationResponse { public int HttpStatusCode { get; set; } public List<AcceptedPromo> PromosAccepted { get; set; } = new List<AcceptedPromo>(); public List<RejectedPromo> PromosRejected { get; set; } = new List<RejectedPromo>(); public string UnhandledErrorBody { get; set; } |
Creating the basic logic for calculating discounts
In my case, the discount calculation logic is located in the CustomPromotionsCommand in the CalculatePromotionIntegration method.
public PromotionIntegrationResponse CalculatePromotionIntegration(PromotionIntegrationPayload payload) { var response = new PromotionIntegrationResponse(); try { HandleBogoPromotion(payload, response); response.HttpStatusCode = (int)HttpStatusCode.OK; return response; } catch (Exception ex) { response.HttpStatusCode = (int)HttpStatusCode.BadRequest; response.UnhandledErrorBody = JsonConvert.SerializeObject(ex); return response; } } |
The calculation and processing of the buy-one-get-one-free promotion have been made into a separate method since the integration promotion can be used by several promotions simultaneously.
private static void HandleBogoPromotion(PromotionIntegrationPayload payload, PromotionIntegrationResponse response) { var requestedBogoPromotion = payload.PromosRequested.FirstOrDefault(x => x.xp.Type == PromotionTypeEnum.BOGO); if (requestedBogoPromotion == null) { return; } var lineItemsToProcess = payload.LineItems .IntersectBy(requestedBogoPromotion.EligibleLineItemIDs, x => x.ID).ToList(); if (!lineItemsToProcess.Any()) { return; } var discounts = CalculateDiscounts(lineItemsToProcess); response.PromosAccepted.AddRange(BuildAcceptedPromos(requestedBogoPromotion.ID, discounts)); } |
In this method, we check whether this promotion has been applied to the order and whether there are any LineItems for which discounts need to be calculated. The calculation logic itself is located in the CalculateDiscounts method.
private static List<ItemWithAmount> CalculateDiscounts(List<HSLineItem> lineItemsToProcess) { var productsWithPrices = lineItemsToProcess .SelectMany(lineItem => Enumerable.Repeat( new ItemWithAmount(lineItem.ID, lineItem.UnitPrice!.Value), lineItem.Quantity)) .OrderBy(item => item.Amount) .ToList(); var discountItemsCount = productsWithPrices.Count / 2; var productsToDiscount = productsWithPrices.Take(discountItemsCount); var discountedItems = productsToDiscount .GroupBy(item => item.Id) .Select(group => new ItemWithAmount(group.Key, group.Sum(item => item.Amount))); return discountedItems.ToList(); } |
Here, first we divide the LineItems into individual products according to their quantity and the cost of one product, and sort them in ascending order of price, so we get a list of products and their prices from the cheapest to the most expensive.
Next, according to our task, we need to get a discount on half of the cheapest products; we calculate their number and get a list of them.
After we have received the list of products for which we should receive a discount, we group them by the LineItemID and get the total discount for each LineItem.
Thus, this CalculateDiscounts method will return us a list of the LineItems that receive a discount and the amount of this discount.
Next, this list is transformed into a list of objects of the AcceptedPromo type, which we must return to the OrderCloud.
private static List<AcceptedPromo> BuildAcceptedPromos(string promotionId, List<ItemWithAmount> lineItemsWithDiscounts) { return lineItemsWithDiscounts.Select(x => new AcceptedPromo() { ID = promotionId, LineItemID = x.Id, Amount = x.Amount }).ToList(); } |
Calculation result
As a result, when two products in this category are added to the cart, the second cheaper one will have a 100% discount, that is, it will be free.