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.
Last updated: Oct 24, 2016