Join us this Friday for AI in Action at the Virtual Happy Hour! This free virtual event is open to all—enroll now on Academy and don’t miss out.
Join us this Friday for AI in Action at the Virtual Happy Hour! This free virtual event is open to all—enroll now on Academy and don’t miss out.
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.
Classes in this topic are available in the following namespaces:
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:
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.
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
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:
Last updated: Oct 24, 2016