ECF’s promotion engine is built on the Windows Workflow Rules Engine. ECF comes with several custom promotions which use this engine. In addition, you can create your own promotions which access ECFs promotion engine. There are two things that need to be built when creating a new promotion : a config file to create a template (also more commonly known as an expression) for a promotion and a configuration control to allow users to create as many unique instances of the promotion from the template as they want in Commerce Manager. For example, you might create a template for a promotion where:
- When a user buys product X
- The price of product Y will be reduce by Z percent
Once this template is created, a configuration control is created to allow users to 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, several things should be considered. One is whether there is an existing promotion built-into ECF that accomplishes most of what you already want to do. For most new custom promotions, built-in promotions included in ECF will include most of the functionality needed. Often copying an existing promotion to create a new promotion will provide 70-90% of the needed logic. Another consideration is that the design of the promotion should be clear first before developing it.
It's easiest to begin by developing the configuration control. When completed, the configuration control will provide the logic and User Interface which will allow users to set the variable values, so to speak, for the promotion rules template (aka expression). The control will be displayed in the area highlighted in this image.
There is a general pattern to the configuration control. Here are the elements of that pattern:
- The control needs to be placed in the following location: ConsoleManager/Apps/Marketing/Promotions/[YourCustomPromotionName].
- The ascx must be named ConfigControl.ascx (although the code behind class must be unique of course).
- The control must inherit from Mediachase.Web.Console.BaseClasses.PromotionBaseUserControl
- The control class must contain a serializable class called Settings. This class will contain the properties that will be used for setting the variable values for the formula (in the template/expression). Here’s an example of the Settings class:
- The control must override the SaveChanges() method from the base class.
- In addition, the DataBind() method should be overriden. The DataBind method should be used to set the properties in the control to the previously saved values (if applicable).
From the examples Settings class, there are several settings you’ll usually want to have for most promotions. Note that you don’t have to use these variable names – that’s up to you and they can be anything. However, they are used in the built-in promotions and will probably be useful for your first couple of promotions you create.
- RewardType. This is a string type and the three types used in the ECF engine are distinguished in the PromotionRewardType. The three types are : WholeOrder, AllAffectedEntries, EachAffectedEntry.
- WholeOrder applies a discount to the top-level properties of an order, namely the charge for a shipment or to the subtotal. Use WholeOrder for promotions which apply to either shipments or to adjustments to the subtotal.
- AllAffectedEntries is intended to calculate the total discount for affected SKUs as a whole. An example of this might be : When 2 or more of SKU x is purchased, $50 is deducted from the subtotal.
- EachAffectedEntry is inteded to calculate a discount which is applicable to each applicable SKU in the cart. An example of this might be: When buying 2 or more of SKU x, get $20 off each SKU y purchased.
- AmountOff. This is used to store the % or amount off.
- AmountType. This settings determines whether the AmountOff property is a % or $ value. The PromotionRewardAmountType, which has enumerations “Value” and “Percentage”.
The Settings class should contain all of the properties set by the user in the control and any other settings necessary for your promotion.
Information for promotions is saved in the PromotionDto. The base class makes an instance of the particular promotion’s data available in the “PromotionDto” variable; you don’t 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 customer’s settings (e.g. settings.MinimumQuantity = decimal.Parse(txtMinimumQuantity.Text);). Here’s an example of this:
In the SaveChanges method, the Setting class values should then be serialized (using the SerializeSettings method in the base class), and then saved in a Dto used to store promotion details. This might look like this:
You don’t have to save the promotion settings explicitly (this is done by Commerce Manager). Note also that the promotion row has an OfferAmount and an OfferType properties, which may be duplicate to some of your Setting parameters. They may look like this :
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. For now, the syntax you need to include is:
Note the base.Config property. It contains a reference to the configuration file or template you’ll be creating in the next step. Part of the configuration file is the actual expression which is the template.
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. Here’s an example of binding the controls:
- The base class Config property gives you access to information about the expression definition (more on this later).
- The base class PromotionDto property provides access to the promotion settings.
- The Settings object can be deserialized by calling the DeserializeSettings() method.
Creating the Template, AKA Expression
The template consists of an xml file with 6 subelements. It looks like this:
Five of the six subelements require no further explanation. Note that all of these properties are available in the ConfigControl.ascx.cs as a base property called “Config”. The expression is a lengthy XAML statement that you won’t edit directly. Instead, you’ll use a tool included with ECF, the Expression Editor. When you’re creating a promotion, you will generally want to use an existing promotion and modify its contents for your new promotion. Here are the steps to accomplish this:
- Copy the XAML for the expression of an existing template (everything inside and not including the <Expression> open and close tags.
- Open ExpressionEditor. It can be found in BusinessLayer/Marketing/ExpressionEditor/bin/Debug/ExpressionEditor.exe.
- Paste the XAML into the expression textbox.
- Click the “Edit Expression” button. The Rule Set Editor (a .NET 3.5 tool) will appear with the expression in rule form.
- Make changes to the expression (more on this shortly).
- Click the OK button for the Rule Set Editor, closing the window.
- Click the Copy button of the ExpressionEditor.
- Paste the next expression into your new template xml file.
Before editing the rules there are a few important things to understand as it is probably a new development environment for you.
- 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 ECF API for any subsistence if you need to
- Each line, separated by a carriage return constitutes a different line of code (“;” is not required at the end of each line).
- From the RSE, when you type “this.”, you’re 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:
- 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 be assigning variables
- Next rules(s) should be determining whether the conditions of the promotion are met and the promotion amount
- The final rule should be creating a new PromotionItemRecord which indicates the type and amount of the promotion.
To illustrate how to use and create rules, here's a walkthrough portions of the OrderVolumeDiscount promotion.
Remember that the expression is the template for multiple versions of the same promotion. So when you’re assigning variables, you’re assigning the promotion-admin-specified parameters. Here’s what this rule might look like:
First, look at the If statement. “If true” will always return true which means the Then statement will always execute and the Else will never execute. Also note that, for this statement to execute first, all other rules must have a numerically lower priority. The name is not significant to the rules but can help you to know the purpose of the rule. So where do the “$..” values come from (e.g. “..= "$CategoryCode"”)? These are the exact names of the parameters that are 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:
Replace is base class method which has two parameters : the expression and the Settings class instance for that promotion. The Replace class will look for all instance of the variable names from the Settings instance (with a $ in front of it) in the expression and replace them with the value from the Settings instance. All of the values are set as strings in the expression instance. So, if a new promotion is created with the above variables, you should have a Settings class definition something like:
This means there’s 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 100j and the RewardType and AmountType are left as is, the Replace function will expression instance that will look like this (or at least this will be part of the instance):
Notice that the RewardType and AmountType values 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 to see this.
Here’s an example of a test of whether the order meets the conditions of the promotion:
Notice that the Priority is a lower number, meaning it will execute 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 order’s 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 doesn’t 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, meaning nothing is done and the next rule is executed.
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 that the [reduced] syntax PromotionItem = this.PromotionContext.CurrentPromotion isn’t quite intuitive. This statement is doing two things. One is that its adding a PromotionItemRecord to the current context. The second is that its 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.
Here is the SetupConstants expression for the 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 the SourceEntriesSet.MakeCopy() method is called, 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 (#,$& etc) to delimit the list of entries. See the ConfigControl for BuyXGetNofYatReducedPrice for an example of how to build this variable string.
Here's another example of the Promotion Condition Test:
This example uses a method of the RulesContext object, GetCollectionSum. In this case, we're 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.
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.
Once you've created you promotion and are testing how it works with you environment, there are a few tips that will 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 of the caching to “0:0:0”.
- Remember that for version 5.0 and 5.1, the RewardType for shipping discounts has to be of type PromotionRewardType.WholeOrder and the amount type must be of type PromotionRewardAmountType.Value.
- The execution of the rules in the rules engine can’t be debugged directly. However, there are several locations where breakpoints will allow you to determine the inputs and outputs into the rules engine for your promotion.
- Place a breakpoint in BusinessLayer/Marketing/MarketingRuleValidators/RulesExprValidator.cs, in the getter for the RuntimeContext property. This will allow you to see exactly what parameters are being passed into the rules engine from the promotion settings (which were saved by the user in defining the promotion in Commerce Manager). This is a dictionary that is used to pass in the promotion definition Settings variable values into the rules engine.
- Also BusinessLayer/CommerceLib/Marketing/MarketingContext.cs, EvaluatePromotions(PromotionContext context, PromotionItemCollection promotions, PromotionFilter filter) method. This is called by the workflow activity CalculateDiscountActivity.
- Also BusinessLayer/ActivityLibrary/Cart/CalculateDiscountActivity.cs, in the catch portion of the try stament of the Execute method. A lot of errors are caught and thrown without the exception details in the workflow. For debugging, add an (Exception ex) to the catch and then inspect the errors.