Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

Loading...
Applies to versions: 10-12.x
Other versions:
ARCHIVED This content is retired and no longer maintained. See the version selector for other versions of this topic.

Using calculate discount activity [Legacy]

Recommended reading 

This topic shows how to work with the legacy Calculate Discount activity in Episerver Commerce. The activity is found in the ActivityFlows/Legacy/LegacyCartValidateActivityFlow.cs, but the actual work is done with the CalculateDiscounts() method.

How it works

Classes in this topic are available in the following namespaces:

  • Mediachase.Commerce.Orders
  • Mediachase.Commerce.Workflow.Activities.Cart
  • Mediachase.Commerce.Website.Helpers
  • Mediachase.Web.Console.BaseClasses
  • Mediachase.Commerce.Marketing.Dto
  • Mediachase.Commerce.Marketing.Validators

Implementation example

The work includes setting up a new context to keep track of results.

Example: setting customer segment and customer promotion history contexts

private void InitMarketingContext()
            {
                OrderGroup group = this.OrderGroup;
                SetContext(MarketingContext.ContextConstants.ShoppingCart, group);

                // Set customer segment context
                MembershipUser user = SecurityContext.Current.CurrentUser;
                if (user != null)
                {
                    CustomerProfileWrapper profile = SecurityContext.Current.CurrentUserProfile as CustomerProfileWrapper;

                    if (profile != null)
                    {
                        SetContext(MarketingContext.ContextConstants.CustomerProfile, profile);

                        CustomerContact customerContact = CustomerContext.Current.GetContactForUser(user);
                        if (customerContact != null)
                        {
                            SetContext(MarketingContext.ContextConstants.CustomerContact, customerContact);

                            Guid accountId = (Guid)customerContact.PrimaryKeyId;
                            Guid organizationId = Guid.Empty;
                            if (customerContact.ContactOrganization != null)
                            {
                                organizationId = (Guid)customerContact.ContactOrganization.PrimaryKeyId;
                            }

                            SetContext(MarketingContext.ContextConstants.CustomerSegments, MarketingContext.Current.GetCustomerSegments(accountId, organizationId));
                        }
                    }
                }

                // Set customer promotion history context
                SetContext(MarketingContext.ContextConstants.CustomerId, SecurityContext.Current.CurrentUserId);

                // Now load current order usage dto, which will help us determine the usage limits
                // Load existing usage Dto for the current order
                PromotionUsageDto usageDto = PromotionManager.GetPromotionUsageDto(0, Guid.Empty, group.OrderGroupId);
                SetContext(MarketingContext.ContextConstants.PromotionUsage, usageDto);
            }

Begin by setting the running total to the order subtotal.

Example: setting running total to order subtotal

Dictionary<string, object> context = MarketingContext.Current.MarketingProfileContext;

        // Parameter that tells if we need to use cached values for promotions or not
        bool useCache = false;

        // Constract the filter, ignore conditions for now
        PromotionFilter filter = new PromotionFilter();
        filter.IgnoreConditions = false;
        filter.IgnorePolicy = false;
        filter.IgnoreSegments = false;
        filter.IncludeCoupons = false;

        // Get property
        OrderGroup order = this.OrderGroup;

        // Create Promotion Context
        PromotionEntriesSet sourceSet = null;

        // Reuse the same context so we can track exclusivity properly
        PromotionContext promoContext = new PromotionContext(context, new PromotionEntriesSet(), new PromotionEntriesSet());
        promoContext.PromotionResult.RunningTotal = order.SubTotal;

Populate a PromotionEntriesSet with the order form and its line items (only applicable if the line item does not have a gift discount associated with it). This is the source set, which the promotion context needs to evaluate a promotion.

Example: populating PromotionEntriesSet

foreach (OrderForm form in order.OrderForms) // Nominal, since there is only one order form per order group
        {
            foreach (OrderFormDiscount discount in form.Discounts)
            {
                if (!discount.DiscountName.StartsWith("@")/* && discount.DiscountId == -1*/) // ignore custom entries
                    discount.Delete();
            }

            // Create source from current form
            sourceSet = CreateSetFromOrderForm(form);

            ...

For entry discounts only

You can limit entry promotions to the quantity applied. For example, if you add to cart a line item of SKU X, with quantity 3, that qualifies for an entry-level promotion, you can limit the promotion to 1 unit of the line item. This results in the customer buying 3 units of SKU X and receiving a discount for one unit. For details, see the Marketing section of the Episerver User Guide.

entryDiscountApplicationCount

The Dictionary object keeps track of the quantity of entries that qualify for one or more promotions. The promotion engine first evaluates line items for any qualifying promotions, then retroactively examines the results to apply the entry limit. For example, you have a promotion X, which SKU Y qualifies for, that limits the entry discount to quantity of 1. When you add SKU Y to the cart, the promotion engine evaluates promotion X to be valid for SKU Y. After all line items in the cart have been evaluated for valid promotions, the promotion engine takes one last step to ensure that any promotion reward for promotion X only gives a discount for one unit of SKU Y, regardless of the line item quantity for SKU Y.

The use for this is more apparent when you have multiple line items, all qualifying for the same promotion. If you set a limit to 3 and have 5 line items that all qualify for the same promotion, you only get discounts for the 3 line items with the highest placed prices.

Note: The entry discount limit, as described above, applies per run of CalculateDiscountsActivity. In other words, this is not a limit that persists over multiple runs of the activity nor carts/orders.

The line items are ordered by placed price in descending order, so that entry discounts are applied to maximize savings for the customer.

    // Build dictionary to keep track of entry discount limit
    Dictionary<PromotionDto.PromotionRow, decimal?> entryDiscountApplicationCount = new Dictionary<PromotionDto.PromotionRow, decimal?>();
    PromotionDto promotionDto = PromotionManager.GetPromotionDto(FrameworkContext.Current.CurrentDateTime);
    foreach (PromotionDto.PromotionRow promotion in promotionDto.Promotion)
    {
        if (!promotion.IsMaxEntryDiscountQuantityNull())
        {
            entryDiscountApplicationCount.Add(promotion, promotion.MaxEntryDiscountQuantity);
        }
    }

    IOrderedEnumerable<LineItem> highPlacedPriceFirst = form.LineItems.ToArray().OrderByDescending(x => x.PlacedPrice);
    int lineItemCount = highPlacedPriceFirst.Count();
    int i = 0;

}

For each line item in the order form:

  • Delete any discount that is not a custom entry.
  • If the line item is a gift, move to the next line item.
  • Populate a target PromotionEntriesSet with just the current line item.
  • Make sure to set the target group, for instance order, entry, shipping.
  • Evaluate the promotion based on the current promotion context.
  • The result is saved in the RunningTotal field of the PromotionResult member of the promotion context.

Example: removing, populating, setting target group, evaluate and save result for line items

 // Cycle through each line item one by one
foreach (LineItem lineItem in highPlacedPriceFirst)
{
    i++;
    // First remove items
    foreach (LineItemDiscount discount in lineItem.Discounts)
    {
        if (!discount.DiscountName.StartsWith("@")/* && discount.DiscountId == -1*/) // ignore custom entries
            discount.Delete();
    }
    //Exclude gift lineItems from evaluation discounts process
    if (IsGiftLineItem(lineItem))
    {
        continue;
    }

    totalNumberOfItems++;

    // Target only entry promotions
    PromotionEntriesSet targetSet = new PromotionEntriesSet();
    targetSet.OwnerId = form.OrderFormId.ToString();
    //ET [16.06.2009] If order contains two item with same code, in target hit only first
    //targetSet.Entries.Add(sourceSet.FindEntryByCode(lineItem.CatalogEntryId));
    targetSet.Entries.Add(CreatePromotionEntryFromLineItem(lineItem));

    promoContext.SourceEntriesSet = sourceSet;
    promoContext.TargetEntriesSet = targetSet;

    promoContext.TargetGroup = PromotionGroup.GetPromotionGroup(PromotionGroup.PromotionGroupKey.Entry).Key;

    // Evaluate conditions
    bool checkEntryLevelDiscountLimit = i == lineItemCount;
    MarketingContext.Current.EvaluatePromotions(useCache, promoContext, filter, entryDiscountApplicationCount, checkEntryLevelDiscountLimit);

    // from now on use cache
    useCache = true;
}

Do the same as in the previous step, but apply to order level discounts instead.

This time, the source and target entries sets are the same.

  • Evaluate order-level promotion(s).
  • Do the same for shipping promotions, per shipment in the order.
  • Each source/target entries set consists of line items in the shipment we are dealing with, but without gift items.
  • After each evaluation, the promotion is marked as Committed or Invalid, and the running total in the promotion context is updated accordingly.

Example: removing, populating, setting target group, evaluate and save result on order level

#region Determine Order level discounts
        foreach (OrderForm form in order.OrderForms)
        {
            // Now process global order discounts
            // Now start processing it
            // Create source from current form
            sourceSet = CreateSetFromOrderForm(form);
            promoContext.SourceEntriesSet = sourceSet;
            promoContext.TargetEntriesSet = sourceSet;
            promoContext.TargetGroup = PromotionGroup.GetPromotionGroup(PromotionGroup.PromotionGroupKey.Order).Key;
        }

        // Evaluate conditions
        MarketingContext.Current.EvaluatePromotions(useCache, promoContext, filter, null, false);
        //Removing now not aplyied Gift discounts from Order
        RemoveGiftPromotionFromOrder(order, promoContext);

        #endregion

        #region Determine Shipping Discounts
        foreach (OrderForm form in order.OrderForms)
        {
            foreach (Shipment shipment in form.Shipments)
            {
                // Remove old discounts if any
                foreach (ShipmentDiscount discount in shipment.Discounts)
                {
                    if (!discount.DiscountName.StartsWith("@")/* && discount.DiscountId == -1*/) // ignore custom entries
                        discount.Delete();
                }

                // Create content for current shipment
                /*
                sourceSet = CreateSetFromOrderForm(form);
                promoContext.SourceEntriesSet.Entries = sourceSet.Entries;
                 * */
                PromotionEntriesSet targetSet = CreateSetFromShipment(shipment);
                promoContext.SourceEntriesSet = targetSet;
                promoContext.TargetEntriesSet = targetSet;
                promoContext.TargetGroup = PromotionGroup.GetPromotionGroup(PromotionGroup.PromotionGroupKey.Shipping).Key;

                // Evaluate promotions
                MarketingContext.Current.EvaluatePromotions(useCache, promoContext, filter, null, false);

                // Set the total discount for the shipment
                // shipment.ShippingDiscountAmount = GetDiscountPrice(order, promoContext.PromotionResult);
            }
        }

        #endregion

Finally, the discounts are applied to the actual order. If the discount is not a shipping discount, it is added to the line item, and the new discounted price is stored in the ExtendedPrice field.

Example: applying discount to order

#region Start Applying Discounts
        decimal runningTotal = order.SubTotal;
        foreach (PromotionItemRecord itemRecord in promoContext.PromotionResult.PromotionRecords)
        {
            if(itemRecord.Status != PromotionItemRecordStatus.Commited)
                continue;

            // Pre process item record
            PreProcessItemRecord(order, itemRecord);

            // Applies discount and adjusts the running total
            if (itemRecord.AffectedEntriesSet.Entries.Count > 0)
                runningTotal -= ApplyItemDiscount(order, itemRecord, runningTotal);
        }
        #endregion

        #region True up order level discounts
        // (Value based order level discounts are still divided up over line items, so this takes care of the resulting rounding errors)
        decimal orderLevelAmount = 0;
        decimal lineItemOrderLevelTotal = 0;
        foreach (OrderForm form in order.OrderForms)
        {
            orderLevelAmount += form.Discounts.Cast<OrderFormDiscount>().Sum(x => x.DiscountAmount);
            lineItemOrderLevelTotal += form.LineItems.ToArray().Sum(x => x.OrderLevelDiscountAmount);
            if (orderLevelAmount > lineItemOrderLevelTotal)
            {
                form.LineItems[0].OrderLevelDiscountAmount += orderLevelAmount - lineItemOrderLevelTotal;
            }
        }
        #endregion

Combining entry-level and order-level discounts

Order-level discounts look at an untouched order form from the cart in the context first saved and populated, when this activity starts running. If an entry-level discount disables any subsequent order-level discount, this does not take place. Promotions are evaluated cumulatively first. Whatever promotion passes the test is then applied consecutively and cumulatively after the evaluation.

By the time the entry-level discount is applied to the original order form, there is nothing to prevent the order-level discount (that should have been invalidated) from running and applying further, unwanted discounts.

There are a couple of ways to change this:

  • Evaluate and (upon validation) immediately apply discounts after each type/instance of a validated promotion to the cart, so that any subsequent promotion checks its conditions against an updated context.
  • Change sources of information/properties like RunningTotal and RulesContext.PromotionCurrentOrderForm to look at an ever-changing source of information regarding applied promotions.
Do you find this information helpful? Please log in to provide feedback.

Last updated: Oct 24, 2016

Recommended reading