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

Try our conversational search powered by Generative AI!

Loading...
ARCHIVED This content is retired and no longer maintained. See the latest version here.

Recommended reading 

Introduction

This document provides examples of how to work with the Calculate Discount activity. The activity is found in the CartValidate workflow, but the actual work is done with the CalculateDiscounts() method.

Classes referred to here are available in the following namespaces:

Implementation example

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

Example: setting customer segment and customer promotion history contexts

C#
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 sub-total.

Example: setting running total to order sub-total

C#
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 will be the source set, which the promotion context will need to evaluate a promotion.

Example: populating PromotionEntriesSet

C#
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

Entry promotions can be limited in 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 be applied to only 1 unit of the line item. This would result the customer buying 3 units of SKU X and receiving a discount for one unit of the SKU. For details, see the Marketing section of the User Guide.

entryDiscountApplicationCount

The Dictionary object is used to keep track of the quantity of entries that qualify for one or more promotions. The promotion engine first evaluates all line items for any qualifying promotions, then retroactively examines the results to apply the entry limit. For example, you can 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 will only give 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 you have 5 line items all qualifying for the same promotion, you will 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 will be applied to maximize savings for the customer.

C#
    // 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 on 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

C#
 // 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;
}

Inside MarketingContext.Current.EvaluatePromotions,

Do the same as in the previous step, but applied 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 any 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

C#
#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

C#
#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 should disable any subsequent order level discount, this will not take place. All promotions are evaluated cumulatively first. Whatever promotion passes the test will then be applied consecutively and cumulatively after the evaluation.

By the time the entry level discount is applied to the original order form, there is nothing in place 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 will check 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 21, 2014

Recommended reading