Hide menu Last updated: Oct 12 2015

The Episerver Commerce promotion engine is built on the Windows Workflow Rules engine. Episerver Commerce comes with several custom promotions which use this engine. In addition, you can create your own promotions by accessing the promotion engine. This topic explains the main steps for developing and testing customized promotions.

Classes in this topic are available in the following namespaces:

See also Promotions and Creating a Volume Discount Promotion for related information.

How it works

Two things that need to be built when creating a new promotion:

  • A configuration file to create an expression or template, for a promotion.
  • A configuration control to let users create as many unique instances of the promotion from the template as they want in Commerce Manager.

For example, you might create an expression (template) for a promotion where:

  • When a shopper buys product X...
  • ...the price of product Y is reduced by Z percent

After this expression (template) is created, a configuration control is created to let users configure promotions from this template in Commerce Manager. Think of the template as a formula with variables which constitute the promotion and the configuration control as the place to set the value of the variables. The promotion engine calculates the discounts applicable to a cart based on the promotions created in Commerce Manager.

Before developing a promotion, consider the following:

  • Is there is an existing Episerver Commerce promotion that accomplishes most of what you want to do? Often, copying an existing promotion to create a new one provides 70-90% of the needed logic.
  • Is the design of the promotion clear before you start developing it?

Creating the configuration control

Start by developing the configuration control. When completed, the configuration control provides the logic and user interface, which lets users set the variable values for the promotion rules template (expression). In this example, the control is displayed under Purchase Conditions and Rewards in the image.

Developing Promotions

There is a general pattern for elements in the configuration control:

  • Place the control in the following location: ConsoleManager > Apps > Marketing > Promotions > [YourCustomPromotionName].
  • The .ascx must be named ConfigControl.ascx (although the code behind class must, of course, be unique).
  • The control must inherit from Mediachase.Web.Console.BaseClasses.PromotionBaseUserControl.
  • The control class must contain a serializable class called Settings. This class contains properties to be used for setting the variable values for the formula (in the template/expression).

Example: the Settings class

C#
Settings Class Example
[Serializable]
public class Settings
{
    public int MinimumQuantity = 1;
    public string ShippingMethodId = string.Empty;
    public string RewardType = PromotionRewardType.WholeOrder;
    public decimal AmountOff = 0;
    public string AmountType = PromotionRewardAmountType.Value;
}
  • The control must override the SaveChanges() method from the base class. In addition, you should override the DataBind() method.
  • Use the DataBind method to set the properties in the control to the previously saved values (if applicable).

Settings

From the Settings class examples, you need the following settings for most promotions. While you do not have to use these variable names, they are used in the built-in promotions and are useful for the first promotions you create.

  • RewardType. This is a string type, and the types used in the engine are distinguished in the PromotionRewardType. There are three types: WholeOrder, AllAffectedEntries, EachAffectedEntry.
  • WholeOrder applies a discount to an order's top-level properties, namely the charge for a shipment or to the subtotal. Use WholeOrder for promotions which apply to either shipments or adjustments to the subtotal.
  • AllAffectedEntries calculates the total discount for affected SKUs as a whole. Example: when 2 or more of SKU x is purchased, $50 is deducted from the subtotal.
  • EachAffectedEntry calculates a discount applicable to each applicable SKU in the cart. Example: When buying 2 or more of SKU x, get $20 off each SKU y purchased.
  • AmountOff. Stores the % or amount off.
  • AmountType. Determines whether the AmountOff property is a % or currency value. The PromotionRewardAmountType has enumerations Value and Percentage.

The Settings class should contain all properties set by the user in the control, and any other settings necessary for your promotion.

SaveChanges

Information for promotions is saved in the PromotionDto. The base class makes an instance of the particular promotions data available in the PromotionDto variable. You do not have to retrieve the promotion data explicitly in your control. The PromotionDto contains a table called Promotion. One row in the Promotion table represents the settings for a single promotion.

The SaveChanges method is called by ECF when the user selects the OK button at the bottom of a promotion definition. In the SaveChanges method, an instance of the Settings class, should be populated with the customers settings, for instance settings.MinimumQuantity = decimal.Parse(txtMinimumQuantity.Text);.

Example:populating an instance of Settings

C#
Settings settings = new Settings();
settings.MinQuantity = Decimal.Parse(MinQuantity.Text);
settings.AmountOff = Decimal.Parse(OfferAmount.Text);
settings.RewardType = PromotionRewardType.EachAffectedEntry;
int offerType = Int32.Parse(OfferType.SelectedValue);
settings.AmountType = offerType == 1 ? PromotionRewardAmountType.Percentage : PromotionRewardAmountType.Value;

In the SaveChanges method, the Setting class values should be serialized, using the SerializeSettings method in the base class, then saved in a Dto used to store promotion details.

Example: serializing Settings

C#
PromotionDto.PromotionRow promotion = PromotionDto.Promotion[0];
promotion.Params = SerializeSettings(settings);

You do not have to save the promotion settings explicitly, this is done by Commerce Manager. The promotion row has an OfferAmount and an OfferType properties, which may duplicate to some Setting parameters.

Example: OfferAmount and OfferType properties

C#
promotion.OfferAmount = Decimal.Parse(OfferAmount.Text);
promotion.OfferType = Int32.Parse(OfferType.SelectedValue);

When saving a promotion, the expression also needs to be saved. The variable values which are set in the Settings class are used to customize the template/expression. Then, the customized expression is saved in the ExpressionDto as a row in the Expression table of the DTO. This will make more sense after creating your first expression.

Example: creating and saving the expression

C#
// Create custom expression from template
string expr = base.Replace(base.Config.Expression, settings);

// Create or modify expression
ExpressionDto expressionDto = new ExpressionDto();

ExpressionDto.ExpressionRow expressionRow = base.CreateExpressionRow(ref expressionDto);
expressionRow.Category = ExpressionCategory.CategoryKey.Promotion.ToString();
expressionRow.ExpressionXml = expr;
if (expressionRow.RowState == DataRowState.Detached)
    expressionDto.Expression.Rows.Add(expressionRow);

// Save expression
ExpressionManager.SaveExpression(expressionDto);

The base.Config property contains a reference to the configuration file or template you create in the next step. Part of the configuration file is the actual expression, which is the template.

DataBind

When the control renders in Commerce Manager, during the creation or editing of a promotion, the controls need to be initialized and set to the previously saved values, if applicable.

Example: binding the controls

C#
if (Config != null)
{
                txtDescription.Text = Config.Description;
}

if (PromotionDto != null && PromotionDto.Promotion.Count != 0)
{
    PromotionDto.PromotionRow row = PromotionDto.Promotion[0];
    object settingsObj = DeseralizeSettings(typeof(Settings));
    if (settingsObj != null)
    {
        Settings settings = (Settings)settingsObj;

        CategoryXFilter.SelectedNodeCode = settings.CategoryCode;
        MinQuantity.Text = settings.MinimumQuantity.ToString();

        //select the correct shipping method, if it still exists
        if (ddlShippingMethods.Items.FindByValue(settings.ShippingMethodId) != null)
            ddlShippingMethods.SelectedValue = settings.ShippingMethodId;
    }

    txtDiscount.Text = PromotionDto.Promotion[0].OfferAmount.ToString("N2");
    ManagementHelper.SelectListItem(ddlDiscountType, PromotionDto.Promotion[0].OfferType);
}

Note the following:

  • The base class Config property gives you access to information about the expression definition.
  • The base class PromotionDto property provides access to the promotion settings.
  • The Settings object can be deserialized by calling the DeserializeSettings() method.

Creating the expression

The expression, the template for multiple versions of the same promotion, consists of an XML file with 6 subelements.

Example: an expression (template) with subelements

XML
<Promotion sortorder="300">
    <Type>[Unique name with no spaces or special characters]</Type>
    <Name>[Name used for display purposes]</Name>
    <Description>[Short Description]</Description>
    <Group>[this must be one of the following values - shipping, entry, or order]</Group>
    <Path>[relative path to the config control from one folder above Configs folder. For example : BuySKUFromCategoryXGetDiscountedShipping/ConfigControl.ascx</Path>
    <Expression>[This is most of the config file. It consists of XAML which defines the promotion rules] </Expression>
</Promotion>

Five of six subelements require no further explanation.

Note: All properties are available in the ConfigControl.ascx.cs as a base property, called Config. The expression is a lengthy XAML statement that you do not edit directly. Instead, you use a tool included with ECF, the Expression Editor.

When creating a promotion, you generally want to use an existing promotion and modify its contents for your the promotion. These are the steps to accomplish this:

  1. Copy the XAML for the expression from an existing template: everything inside and not including the <Expression> open and close tags.
  2. Open ExpressionEditor, which can be found here.

Developing Promotions

  1. Paste the XAML into the expression text box.
  2. Click Edit Expression. The Rule Set Editor (a .NET tool) appears with the expression in rule form.

Developing Promotions

  1. Make changes to the expression (see below).
  2. Click OK for the Rule Set Editor, closing the window.
  3. Click Copy in the ExpressionEditor.
  4. Paste the next expression into your new template XML file.

Editing the rules

Before editing the rules, there are a few important things to understand:

  • Each rule is effectively an If..Then..Else statement, however its technically Condition..Then..Else. You provide what the If, Then, Else statements are to do. If the If/Condition statement is true, the Then statement executes; otherwise the Else statement executes.
  • If at any point the keyword HALT is executed in an expression, the expression evaluation stops.
  • The Rule Set Editor provides intellisense for the objects accessible to you.
  • You can access the Episerver Commerce API for any subsistence if needed.
  • Each line, separated by a carriage return, constitutes a different line of code (that is, a semocolon (;) is not required at the end of each line).
  • From the RSE, when you type this, you are accessing the default object associated with the promotion rules, Mediachase.Commerce.Marketing.Validators.RulesContext.
  • This object contains a lot of information about the order and current promotion context, including:
    - CurrentCustomerContact
    - PromotionCurrentOrderForm [for evaluating order promotions]
    - PromotionCurrentShipment [for evaluating shipping promotions]
    - PromotionTargetLineItem [for evaluating entry promotions]
    - ShoppingCart [returned as a OrderGroup object, meaning it could be a PurchaseOrder or PaymentPlan also]
    - The rules are executed in order, from highest priority (largest number) to lowest.
  • The general pattern you typically want to follow is:
    - First rule should assign variables.
    - Next rule(s) should determine whether the promotion's conditions are met and the promotion amount.
    - The final rule should create a new PromotionItemRecord, which indicates the promotion type and amount.

Assigning variables

Remember that the expression is the template for multiple versions of the same promotion. When assigning variables, you are assigning the promotion-admin-specified parameters.

Example: assigning variables

C#
Name: SetupContants
        Priority: 2
        Condition: True
        Then
          this.RuntimeContext["RewardType"] = "$RewardType"
          this.RuntimeContext["AmountOff"] = decimal.Parse("$AmountOff", System.Globalization.CultureInfo.InvariantCulture)
          this.RuntimeContext["AmountType"] = "$AmountType"
          this.RuntimeContext["MinOrderAmount"] = "$MinOrderAmount"
        Else
          Halt

In the If statement, If true always returns true, which means the Then statement always executes, and the Else never executes. Also note that, for this statement to execute first, all other rules must have a numerically lower priority. The name, while not significant to the rules, can help you understand their purpose.

The $.. values (for instance ..= "$CategoryCode") are the exact names of the parameters saved in your Settings class in the configuration control. When the following line is executed (part of the configuration control directions before), the template is made into a specific promotion:

C#
// Create custom expression from template
string expr = Replace(Config.Expression, settings);

Replace is a base class method which has two parameters: the expression and the Settings class instance for that promotion. The Replace class looks for all instance of the variable names from the Settings instance (with a $ in front of it) in the expression and replaces them with the value from the Settings instance.

All values are set as strings in the expression instance. If a new promotion is created with the above variables, you should have a Settings class definition that looks something like the example below.

Example: Setting class definition

C#
[Serializable]

public class Settings
{
    public string RewardType = PromotionRewardType.WholeOrder;
    public decimal AmountOff = 0;
    public string AmountType = PromotionRewardAmountType.Percentage;
    public decimal MinOrderAmount = decimal.MaxValue;
}

This means there is a one-to-one match between the variables in the Settings class and the variables you set in this first rule. If you create an instance of this promotion where the AmountOff is 5, the MinOrderAmount is 100, and the RewardType and AmountType are left as is, the Replace function creates an expression instance that looks like the following example.

Example: promotion expression instance

C#
Priority: 2
        Condition: True
        Then
          this.RuntimeContext["RewardType"] = WholeOrder"
          this.RuntimeContext["AmountOff"] = decimal.Parse(5", System.Globalization.CultureInfo.InvariantCulture)
          this.RuntimeContext["AmountType"] = "percentage"
          this.RuntimeContext["MinOrderAmount"] = "100"
        Else
          Halt
The RewardType and AmountType values are resolved to the string values for the enumerations. Also notice that the variables are passed as strings by default. You can cast values as other types and add them to the dictionary associated with the RuntimeContext instance. The dictionary is a string/object paired dictionary so you can store any type of value. You need to cast the object (or use the ToString(), for simple string comparisons with string types in the other rules to access them properly.

The string instance created by the Replace method represents a specific version of this promotion. This value is saved in the database and used during promotion calculations. If you want to see this in action, retrieve the value of the Expression field in the Promotion database table for a particular promotion and load it into the Expression Editor.

Promotion condition test

It is possible to test whether the order meets the conditions of the promotion.

Example: testing the promotion conditions

C#
Name: CheckOrderSubTotal
        Priority: 1
        Condition
        this.PromotionContext.PromotionResult.RunningTotal < decimal.Parse((string)this.RuntimeContext["MinOrderAmount"], System.Globalization.CultureInfo.InvariantCulture)
        Then
          Halt
        Else

Note: The Priority is a lower number, meaning it executes after the SetupConstants rule. The condition is a test to see whether the current RunningTotal property of the PromotionResult object is less than the minimum order specified. The RunningTotal property gives the orders Subtotal, after all previous promotions (if applicable) are applied. The order of promotions applied to orders is specified in the Commerce Manager promotion definition screen ( property). This rule in effect says If the order subtotal is less than the specified minimum order amount, halt the promotion evaluation as it does not apply. The end result is that no discount will be returned by the promotion engine in that case. If, however, the condition is false, meaning the subtotal is equal to or greater than the minimum order amount, there is an empty Else statement. As a result, nothing is done and the next rule is executed.

AssignReward

This is the step where, if the rules have executed to this point, a promotion result needs to be returned. This is a PromotionItemRecord object. The parameter for the AddPromotionItemRecord most commonly used is a new PromotionItemRecord.

The constructor values for a new PromotionItemRecord are:

  • The target entries/SKUs
  • The affected entries/SKUs
  • A new PromotionReward instance, which has two parameters:
    - The amount off value (a decimal)
    - The amount type (percentage or value)

From the AddPromotionItemRecord returned PromotionItemRecord, the PromotionItem is assigned to the PromotionContext.CurrentPromotion, thus passing the promotion information back to the portion of the code that called the rules engine (StoreHelper.cs or CalculateDiscountActivity.cs) to be added to the cart.

Note: The [reduced] syntax PromotionItem = this.PromotionContext.CurrentPromotion is not quite intuitive. This statement is doing two things: adding a PromotionItemRecord to the current context, and setting the PromotionItem associated with that PromotionItemRecord to the current promotion. That PromotionItem contains the properties of the promotion from the database. Finally, the ValidationResult.IsValid = True statement indicates that the result is valid and should be applied.

Example: AssignReward

C#
Name: AssignReward
        Priority: 0
        Condition
            True
        Then
            this.AddPromotionItemRecord(new Mediachase.Commerce.Marketing.Objects.PromotionItemRecord(this.PromotionContext.TargetEntriesSet,
        this.PromotionContext.TargetEntriesSet, new Mediachase.Commerce.Marketing.Objects.PromotionReward((string)this.RuntimeContext["RewardType"],
        (decimal)this.RuntimeContext["AmountOff"], (string)this.RuntimeContext["AmountType"]))).PromotionItem = this.PromotionContext.CurrentPromotion
        this.ValidationResult.IsValid = True

        Else
            Halt

Other examples

Buy X, get Y free promotion

In this case, the PromotionContext.SourceEntriesSet.MakeCopy() method is called. The SourceEntriesSet and TargetEntries set properties are both PromotionEntriesSet types, which represent a collection of entries. When working with entry promotions, the SourceEntriesSet consists of all of the entries associated with the lineItems in an OrderForm.

The TargetEntries consists of one lineitem at a time, with the promotions tested against one LineItem entry at a time. With order promotions, the SourceEntriesSet consists of all of the LineItem entries associated with an OrderForm. With shipment promotions, the TargetEntries consist of all of the LineItem entries associated with a single shipment.

When you call the SourceEntriesSet.MakeCopy() method, it returns the subset of entries specified by the $EntryYFilter parameter. The $EntryYFilter variable is simply a delimited list of entry codes. You can use any special character (#,$& and so on) to delimit the list of entries. See the ConfigControl for BuyXGetNofYatReducedPrice for an example of how to build this variable string.

Example: SetupConstants expression for the buy x, get y free promotion

C#
Priority: 1
        Condition: True
        Then
          this.RuntimeContext["EntryXSet"] = this.PromotionContext.SourceEntriesSet.MakeCopy(new string[] {"$EntryXFilter"})
          this.RuntimeContext["EntryYSet"] = this.PromotionContext.TargetEntriesSet.MakeCopy(new string[] { "$EntryYFilter"})
        Else
          Halt

Promotion condition test

This example uses a method of the RulesContext object, GetCollectionSum. In this case, we are using it to find the total quantity of SKUs from the SourceEntriesSet where the CatalogNodeCode property of the entries is equal to the category code associated with the promotion. If its equal to or greater than the variable minimum quantity, the promotion applies.

The third parameter of the GetCollectionSum allows you to pass in a code expression to use as a filter. The code expression is passed in as a string and turned into a System.CodeDom.CodeExpression. The Windows Workflow Rules engine can then use that expression to filter the IEnumerable Entries collection.

The RulesContext offers a number of methods to allow you to do more efficient evaluations of the cart for promotion condition checks like this using CodeExpressions to filter, validate, or count qualities of the cart.

Example: Promotion condition test

C#
Name: CheckConditionsMet
        Priority: 2
        Condition
          this.GetCollectionSum(this.PromotionContext.SourceEntriesSet.Entries, "Quantity", "this.CatalogNodeCode.ToString().Equals('" + this.RuntimeContext["CategoryCode"].ToString() + "')") < decimal.Parse(this.RuntimeContext["MinQuantity"].ToString()) || this.PromotionCurrentShipment.ShippingMethodId.ToString() != this.RuntimeContext["ShippingMethodId"].ToString()

        Then
          Halt

        Else

There are numerous other promotion examples included in the ECF source code. Review these to better understand other custom promotion options. Another way to get ideas about how to implement your promotion can be found by creating promotions using the Build Your Own Discount option available for entry and order promotions. Create a promotion which includes criteria or rewards similar to your design and then review the expression from the Expression database table in the Expression Editor - it will often provide vital clues for how to complete your promotion.

Debugging tips

After you create your promotion and test it with you environment, the following tips can help you expedite your debugging:

  • Turn off marketing caching during testing. These settings are in the cache node of the ecf.marketing.config file in the Configs folder of the site. Set all caching to 0:0:0.
  • See also CalculateDiscountActivity.cs in the source code package that can be downloaded from world. This actitvity applies discounts, so can point out which promotions were evaluated to true and how they give the discount.

Comments