Do A Lot More With EPiServer Commerce Promotions With a Little Code
The promotion engine is powerful. It allows you to create a broad range of promotions with very different criteria and rewards out of the box. The Build Your Own Discount promotions are especially flexible. Here are some ways that you can extend Build Your Own Discounts to be even more powerful.
1. Promotions based on LineItem metafields : its not obvious how to use LineItem metafields for promotion criteria. For example : For all LineItems in a cart where the color (a LineItem metafield in this example) is green, give $20 off the order total.
2. Promotions based on SKU/Entry metafields : they aren't exposed at all in the dynamic fields of the Build Your Own Discount promotion types. So it doesn't seem possible to provide a discount for all SKUs with the color "Green" and product types of "notebook".
3. Promotions based on product codes rather than SKU codes: When the catalog is made up of products and SKUs, there isn't a visible way of providing discounts for products. For example, buy a shirt product (for which there are SKUs representing different colors and sizes) and get 20% off of the price for any of its associated SKUs.
4. Promotions targeting anonymous customers based on visitor groups or other criteria: Customer Segments are designed to target promotions to logged-in users. There doesn't seem to be a way to target anonymous users based on their visitor group, location information, the online marketing campaign that got them to the site, market, or any other user information.
Here's How You Can Create Each of These:
LineItem metafields
You can create Build Your Own Discounts using the metafield values of the LineItems in a customer's cart.
How to implement:
* Add metafields to the LineItem metaclass for each of the fields you want to use in creating promotion criteria.
* Populate the LineItem metafield when the SKU is added to the cart to create the LineItem. The code to do this looks like:
_cartHelper.AddEntry(entry, quantity, false, new CartHelper[] { _cartHelperToRemove });
//after adding the Entry, get a reference to the lineitem and pass in the metafield value that's used by the promotion
LineItem item = _cartHelper.Cart.OrderForms[0].LineItems[_cartHelper.Cart.OrderForms[0].LineItems.Count - 1];
item["Taste"] = entry.ItemAttributes["Taste"].Value[0];
_cartHelper.Cart.AcceptChanges();
Even better code is coming up to do this more generically..
Explanation:
You may have noticed that lineitem metafields are exposed in the dynamic properties in Build Your Own Discounts. This means that, when an item is added to the cart, the metafield needs to be populated in the LineItem and a promotion will apply to that LineItem where there is a promotion criteria match with that metafield. In the image below, an Order Build Your Own Discount is created with a condition that the lineitem metafield Taste be equal to a value. Note that this only works when discounts are calculated for a cart (for example with the CartValidate or CartPrepare workflows), not for a SKU on a category browse page. When you add new lineitem metafields, make sure you reset IIS so you'll see the new field(s) in Commerce Manager.
You can also do this:
There’s one important caveat regarding using metafields (for entries or lineitems):
The metafield cannot support multiple languages or be a dictionary type. It needs to be a string-type field (e.g. ShortString) and can’t be numeric.
The metafield should be an internal field. For example, you may have a field "Color" which is multi-lingual and used on the front end to display different text based on the language settings for the customer. Then you'd setup a secondary field "CLR" which was used strictly for internal logic purposes, like promotions.
These limitations can be circumvented by using the marketing provider discussed at the end of the article.
Entry metafields
Its possible to target promotions based on Entry metafields, which would allow promotions to apply to SKUs being browsed on the site (not just items in the cart as above). To do this, associate a metafield in the lineitem class to a SKU metaclass as well. Then, when the SKU is added to the cart, make sure the cooresponding metafield is populated in the lineitem.
How to implement:
* Add a metafield to the LineItem metaclass (if it doesn't already exist there)
* Add a metafield to the applicable SKU metaclass (if it doesn't already exist there) where you want to apply a promotion
* Populate the SKU metfield value (if not already populated).
* Make sure the LineItem metafield is updated with the SKU value for the same metafield when added to the cart. See the code above for the LineItem metafield promotions.
Explanation:
If you've looked at the code for calculating discounts, both in Mediachase.Commerce.Website.Helpers.StoreHelper and Mediachase.Commerce.Workflow.Activities.CalculateDiscountsActivity, you'll notice that the an entry or a lineitem can be used to calculate a discount. These can be seen in :
StoreHelper.GetDiscountPrice(Entry entry, string catalogName, string catalogNodeCode, IMarket market, Currency currency)
and
CalculateDiscountsActivity.CalculateDiscounts()
Yet, when you configure a Build Your Own Discount in Commerce Manager, you only see the LineItem metafields listed as possible attributes to map promotion criteria to. Furthermore, the properties of the promotion refer to TargetLineItem. What's happening in both of the aforementioned methods is that many of the base fields and all of the metafield values of the Entry and LineItem objects are being mapped internally to a third class, PromotionEntry. When a discount price is being calculated for an Entry (like on a category landing page), the Entry metafields are added; when the discount price is calculated on a cart, the LineItem metafields are added. The PromotionEntry class doesn't know weather "it" is an Entry or a LineItem. The PromotionEntry is then used by the promotion engine to match up whether a promotion applies to a SKU or LineItem.
Here's a diagram that creates a visual explanation of this:
So you can create a promotion that targets a LineItem metafield that also happens to be shared by an Entry metaclass. If the code calls StoreHelper.GetDiscountPrice() for an Entry that has a metafield "CLR", the LineItem metaclass also has a metafield "CLR", and you've created a promotion with a criteria for CLR to be equal to Green, it will match the Entry metafield value for the Entry used in the StoreHelper.GetDiscountPrice() method.
Here’s some example code for adding all Entry field short string metafield values into matching Lineitem metafield values:
_cartHelper.AddEntry(entry, quantity, false, new CartHelper[] { _cartHelperToRemove });
//after adding the Entry, get a reference to the LineItem and pass
//in the metafield value that's used by the promotion
…
lineItemBeingAdded = _cartHelper.Cart.OrderForms[0].LineItems[_cartHelper.Cart.OrderForms[0].LineItems.Count - 1];
MapEntrytoLineItem(entry, lineItemBeingAdded);
…
/// <summary>
/// Map all matching SKU metafield values (strings only) to matching lineItem metafields
/// </summary>
/// <param name="entry"></param>
/// <param name="item"></param>
private void MapEntrytoLineItem(Entry entry, LineItem item)
{
foreach (ItemAttribute entryAttribute in entry.ItemAttributes.Attribute)
{
foreach (MetaField lineitemAttribute in item.MetaClass.MetaFields)
{
if (lineitemAttribute.Name.Equals(entryAttribute.Name))
{
if (entryAttribute.Type.Equals("ShortString", StringComparison.InvariantCultureIgnoreCase))
{
item[lineitemAttribute.Name] = entry.ItemAttributes[entryAttribute.Name].Value[0];
}
}
}
}
}
Promotions With Product Criteria
If a catalog contains products and SKUs, the current approach to target items in that catalog is either limited to a catalog, category code, or SKU - there isn't a clear way to target products.
This is important if you want to simplify creating Build Your Own Discounts. For example, if you want to target 5 products that have 30 associated SKUs - that's a tedious build your own discount to create. However, there is a way to create promotion criteria that map to product codes. To add this, you can simply add a metafield to the SKU metaclass definition, call it something like ProductCode, and populate it with the parent product code. Setting the metafield value can either be done to the database definition of the SKU or dynamically, during runtime, immediately before calling the StoreHelper.GetDiscountPrice() or running one of the workflows. There also needs to be a matching product code metafield on the LineItem metaclass. And as before, the Entry metafield value needs to be mapped to the lineitem metafield when the item is added to the cart. Then you can write promotions that target products.
Customer Targeting
If you want to target particular customers for your promotions, you're currently seemingly limited to customer segment configuration in promotions and promotion campaigns. And customer segments require the user to be logged in and to have provided information like their email or address.
However, there is a way to inject customer information into a SKU or lineitem to allow that information to be used as a promotion criteria. To do this, you need to add a metafield to the Entry object in memory before calling StoreHelper.GetDiscountPrice() or executing a workflow.
The ItemAttributes portion of the Entry object is settable. So you can retrieve the built-in collection of metafield values from ItemAttributes and add your own metafields and values. So you could do something like:
List<ItemAttribute> attributeList = new List<ItemAttribute>(entry.ItemAttributes.Attribute);
ItemAttribute visitorGroupAttribute = new ItemAttribue();
visitorGroupAttribute.FriendlyName = "VisitorGroup";
visitorGroupAttribute.Name = "VisitorGroup";
visitorGroupAttribute.Type = "ShortString";
//the value set in this metafield can be a visitor group lookup or other customer-attributes
visitorGroupAttribute.Value = new string[] { "MyVisitorGroup" };
attributeList.Add(visitorGroupAttribute);
entry.ItemAttributes.Attribute = attributeList.ToArray();
var discountPrice = StoreHelper.GetDiscountPrice(entry, string.Empty, string.Empty, _marketId);
To be able to use this new metafield, you'd need to add a new metafield named "VisitorGroup" to the LineItem metaclass. When the entry is added to the cart, make sure the LineItem metafield is populated. Now you'll be able to create Build Your Own Discounts that can target different anonymous users based on their associated Visitor Groups or geo-location lookup location or whatever other criteria you want to you.
Extending the Promotion Engine Further
There's another alternative to implementing this logic. You can control how the PromotionEntry object is populated in the StoreHelper.GetDiscountPrice() method and CalculateDiscountActivity before its used to determine whether an Entry or Cart or LineItem meets a promotion's criteria. The EPiServer Commerce system has a provider model for populating the PromotionEntry object. It allows you to explicitly set what fields are added to the PromotionEntry object.
This approach allows you to circumvent the aforementioned limitations where non-string values don't work in promotions and dictionaries can't be used. The reason non-string values don't currently work is because the string representation of all metafield values is currently injected into the PromotionEntry object. However, in your own implementation, you can pass the correct data types in for those metafields you want to use in your promotions. Once you've done this, you'll be able to do things like create a criteria where the container size is greater than 10.
You can use the marketing provider model to :
- add customer segment information like visitor groups or online marketing campaign ids
- decide what dictionary values are added and in what format
- decide what localized version of a field is used in the PromotionEntry
- add values in their explicit type
- create composite fields (using the data from several metafields and/or data sources to create a custom field value)
- and other things no one has thought of.
To use this provider model:
* Create a new class project
* Add a new class to this project that inherits from IPromotionEntryPopulate. (See an example implementation here)
* Add your custom settings to the IPromotionEntryPopulate implementation
* Create a reference in your web site to this new project
* Modify the Configs/ecf.marketing.config file (PromotionEntryPopulateFunctionType element) to map to this new dll and class
* Create LineItem metafields that map to these new settings
* If using this with SKU promotions too, then add the metafields to the SKU metaclass as well. And populate the LineItems with the SKU values when they're added to the cart.
* Now you can create build your own discount promotions that use these custom properties.
An example implementation of the IPromotionEntryPopulate is provided here: http://world.episerver.com/Code/Shannon-Gray/Promotion-PromotionEntry-Provider-Implementation-Example/
It uses the implementation built-in to the API with some example added fields. There's not a lot to explain in the implementation. The class simply passes values from either a LineItem or an Entry object into the PromotionEntry dictionary (
All of these approaches will take more time thinking about and designing then any of the coding will take. Hopefully this help you create more customized promotions quickly and easily in EPiServer Commerce.
Awesome!!!