Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Custom promotions

Describes how to work with custom promotions in Optimizely Commerce Connect.

When you create a new promotion type, also create the following:

  • PromotionData – Includes the metadata needed to run the promotion, represents a content-based promotion item.
  • A processor – Evaluates whether to apply the promotion.

Classes in this topic are available in the following namespace:

  • EPiServer.Commerce.Marketing

PromotionData

PromotionData should contain properties needed to evaluate an order and apply a reward. For example, a PromotionData contains entries to which the promotion applies and the promotion. Add any property a marketer must edit when setting up a promotion to this type. The base class provides basic metadata properties, such as name and valid dates.

PromotionData can be one of the following types.

📘

Note

When creating a promotion, always inherit from one of these, never from PromotionData directly.

  • EntryPromotion – Applies reward to a specific entry.
  • OrderPromotion – Applies reward to the entire order.
  • ShippingPromotion – Applies reward to order's shipping cost.

Connect a description to a property using colors

You can use a color to connect a property to a specific part of a promotion description. Colors make it easier to communicate the meaning of each property in the UI. Add the PromotionRegion attribute to a promotion property to mark it for inclusion in the connection to the description. You can also use the PromotionRegion attribute on a block property to connect properties in a block to the same part of the promotion description.

The attribute constructor takes a single string parameter that determines the property's color. Some pre-defined constants of the type EPiServer.Commerce.Marketing.PromotionRegionName are used by internal promotion types. You should also use those constants when creating custom promotion types.

As with all content types, you can translate the name and description by adding an element to the XML file whose name matches the content type. You can use a new formdescription sub-element to connect properties with the description. Add {regionname}some text{/} in the text to apply the region color to some text. Both {regionname} and {/} are removed before the UI displays the description.

The following example shows a custom promotion type with two properties: one block and one number. ConditionBlock is colored in the UI through a CSS class indicating it is a condition, while Percentage gets a CSS class indicating it is a reward.

[ContentType(GUID = "76EBFEFF-2CFB-42F2-B4A3-EA5EA5A41515")]
public class CustomPromotion: EntryPromotion {
  [PromotionRegion(PromotionRegionName.Condition)]
  public virtual CustomPromotionBlock Conditions {
    get;
    set;
  }

  [PromotionRegion(PromotionRegionName.Reward)]
  public virtual int Percentage {
    get;
    set;
  }
}

[ContentType(GUID = "15B7BEA8-967A-4C5C-87F3-7346E71CBCC9")]
public class CustomPromotionBlock: BlockData {
  public virtual int RequiredQuantity {
    get;
    set;
  }
  public virtual IList<ContentReference> Targets {
    get;
    set;
  }
}

The description is specified in the formdescription element in the translation xml.

<custompromotion>
  <name>My custom promotion</name>
  <create see="/contenttypes/custompromotion/name" />
  <description>Buy at least X items from categories or entries and get a % discount on the cheapest item.</description>
  <formdescription>Buy {condition} at least X items from categories or entries{/} and get {reward}a % discount{/} on the cheapest item.</formdescription>
</custompromotion>

Promotion processors

After you create promotion data, create a processor.

📘

Note

If you inherit a promotion data only to add properties, not for evaluation, you can reuse the base promotion's processor.

The processor evaluates whether a promotion should apply a reward to an order. You can implement the IPromotionProcessor interface directly, but the recommended approach is to inherit from the abstract EntryPromotionProcessorBase<TEntryPromotion>, OrderPromotionProcessorBase<TOrderPromotion>, or ShippingPromotionProcessorBase<TShippingPromotion>, depending on the promotion type.

PromotionProcessorBase has one abstract method to implement: Evaluate. The method is supplied with a PromotionData and a PromotionProcessorContext object that contains information about the current order. It is responsible for determining whether a reward should be applied. The Evaluate method returns a RewardDescription.

RewardDescription

A reward description contains information about if and how a reward is applied. Its most important properties are:

  • A list of redemption descriptions, one for each of the maximum amount of redemptions that could be applied to the current order. The list can ignore redemption limits because the promotion engine handles them.
  • RewardType – Depending on the type, the promotion value is read from the UnitDiscount, Percentage, or Quantity property.
  • Status flag – Indicates whether a promotion is not, partially, or fully fulfilled.
  • SavedAmount – The amount by which this reward reduces the order cost. The promotion engine sets this value; do not set it in the promotion processor.

RedemptionDescription

A RedemptionDescription describes one redemption of a reward. Its primary goal is to identify the objects to which the redemption should apply. The RedemptionDescription also determines how much this redemption saves on the order, and has a status flag that is set when the promotion engine decides not to apply this redemption. Most commonly, that is because a redemption limit was reached.

Depending on which type of promotion the reward gives (entry, order, or shipping), different types of affected objects are used. They can be found in either AffectedEntries, AffectedShipments, or AffectedOrders. Use the CreateRedemptionDescription method on the promotion processor base classes to populate the redemption with the correct type of affected objects.

Affected entries

The promotion engine creates a price matrix for all items in an order form. Working with line item quantities while evaluating and applying discounts can become complicated and create numerous edge cases. The price matrix encapsulates this complexity and helps avoid pitfalls with multiple redemptions and multiple promotions affecting the same line items and gift items.

The price matrix, OrderFormPriceMatrix, is accessible through the EntryPrices property of the PromotionProcessorContext object. PromotionProcessorContext is passed to the Evaluate method as one of the arguments.

The matrix remembers which codes, and the quantity for those codes, were received. The second ExtractEntries call starts to receive entries where the first call ended. This makes it easy to create several redemptions by calling ExtractEntries in a loop, and create one RedemptionDescription inside the loop.

Extract entries

The price matrix has one public method, ExtractEntries, with two overloads. Both overloads take entry codes and quantity as parameters. One of them also contains an action for getting the entries in a specific sequence. If no sequence is specified, MostExpensiveFirst is used.

When you call ExtractEntries, the state change is similar to popping an item from a queue.

  • entryCodes – An IEnumerable<string> that defines the entry codes to receive. The items in the order, which were defined in the condition part of the promotion data, are normally used as entry codes.
  • quantity – Defines the number of items to receive. In the "Buy 3, get the cheapest for free" example, the quantity is 3.
  • sortMethod – An action that defines a custom ordering of the affected entries. The order can matter — for example, only the top 3 items are received in the "Buy 3, get the cheapest for free" scenario. Two predefined sort orders are available: MostExpensiveFirst and CheapestFirst. Both are static methods on the OrderFormPriceMatrix class, which is accessible through the EntryPrices method on PromotionProcessorContext.

Set promotion range

SetDiscountRange defines which affected entries, received from the price matrix, should receive a discount. The method has two parameters: skip and take. The example "Buy 3, get the cheapest for free" should make the call SetDiscountRange(2, 1), which skips the first two items, and gives the promotion (free) to the third. If all items get the promotion, for example 20% off all items, do not call SetDiscountRange.

Promotion engine priority handling

The Discount Priorities view lets a merchandiser manage priority and exclusivity for promotions. Using this view, a merchant can indicate priority order (if an item is eligible for multiple promotions) and which promotions cannot be combined with the current one. The view uses the following PromotionData properties.

  • Priority – Higher-priority promotions are evaluated before lower-priority ones.
  • ExcludedPromotions – List of excluded promotions; ignored when the promotion is evaluated.

Add money collections to your promotion

A promotion may need a collection of currencies and amounts as part of the condition evaluation or reward logic. To achieve this, add an IList<Money> property to the custom promotion class. When used on a promotion type, such a Money collection is tightly coupled with the currencies available on the parent campaign.

Initially, currencies related to the promotion's campaign market have an amount of zero. Changing a campaign's market also changes available currencies in the property. Consider this when developing promotion processors, because you must decide the desired behavior when one, or even all, amounts are set to zero.

The same applies to the MonetaryReward type, which uses an IList<Money> property to store some of its values.

Add help text for custom groups

To include help text for custom groups on a campaign or promotion form, add the Display attribute with the GroupName property set to the name of a specific node within the <groups> section in the resource files. The groups sections are content-type-specific, so place them under the <contenttypes> node for your specific type.

[Display(GroupName = "MyNewGroup")]
public virtual decimal Money { get; set; }

The resource file looks like this:

<contenttypes>
  <nameofcontenttype>
    <groups>
      <mynewgroup>
        <help>This is help text for my new group</help>
      </mynewgroup>
    </groups>
  </nameofcontenttype>
</contenttypes>

See also Property attributes.

Get promotion items

Use GetPromotionItems to display the items included in a promotion or campaign. Use the GetPromotionItemsForCampaign extension method on IPromotionEngine to do the same for an entire campaign. You might want to create a landing page for the campaign or highlight campaign products in listings.

Unlike Evaluate, this method does not consider the cart. Instead, it returns all items that could be discounted. Displaying this information on the website encourages buyers to spend more.

Promotion processor example

The following example shows a percentage-based promotion processor.

using EPiServer.Commerce.Extensions;
using EPiServer.Commerce.Marketing;
using EPiServer.Framework.Localization;
using EPiServer.ServiceLocation;
using System.Collections.Generic;
using System.Linq;

namespace CodeSamples.EPiServer.Commerce.Marketing {
  #region PercentagePromotionProcessorSample

    /// <summary>
    /// Sample of a promotion processor for <see cref="PercentagePromotionSample"/>.
    /// </summary>
    [ServiceConfiguration(Lifecycle = ServiceInstanceScope.Singleton)]
  public class PercentagePromotionProcessorSample: EntryPromotionProcessorBase<PercentagePromotionSample> {
    private readonly CollectionTargetEvaluator _targetEvaluator;
    private readonly FulfillmentEvaluator _fulfillmentEvaluator;
    private readonly LocalizationService _localizationService;

    /// <summary>
    /// Creates a new instance of a <see cref="PercentagePromotionProcessorSample"/>.
    /// </summary>
    /// <param name="targetEvaluator">The service that is used to evaluate an order against a promotion's target properties.</param>
    /// <param name="fulfillmentEvaluator">The service that is used to evaluate the fulfillment status of the promotion.</param>
    /// <param name="localizationService">The service that is used to get localized strings.</param>
    public PercentagePromotionProcessorSample(
      CollectionTargetEvaluator targetEvaluator,
      FulfillmentEvaluator fulfillmentEvaluator,
      LocalizationService localizationService) {
      _targetEvaluator = targetEvaluator;
      _fulfillmentEvaluator = fulfillmentEvaluator;
      _localizationService = localizationService;
    }

    /// <summary>
    /// Evaluates a promotion against an order form.
    /// </summary>
    /// <param name="promotionData">The promotion to evaluate.</param>
    /// <param name="context">The promotion processor context.</param>
    /// <returns>
    /// A <see cref="RewardDescription" /> telling whether the promotion was fulfilled,
    /// which items the promotion was applied to and to which discount percentage.
    /// </returns>
    protected override RewardDescription Evaluate(PercentagePromotionSample promotionData, PromotionProcessorContext context) {
      var lineItems = GetLineItems(context.OrderForm);
      var condition = promotionData.Condition;
      var applicableCodes = _targetEvaluator.GetApplicableCodes(lineItems, condition.Items, condition.MatchRecursive);
      var fulfillmentStatus = _fulfillmentEvaluator.GetStatusForBuyQuantityPromotion(
        applicableCodes,
        lineItems,
        condition.RequiredQuantity,
        condition.PartiallyFulfilledThreshold);
      var affectedEntries = context.EntryPrices.ExtractEntries(applicableCodes, condition.RequiredQuantity);
      return RewardDescription.CreatePercentageReward(
        fulfillmentStatus,
        GetRedemptions(applicableCodes, promotionData, context),
        promotionData,
        promotionData.PercentageDiscount,
        fulfillmentStatus.GetRewardDescriptionText(_localizationService));
    }

    /// <summary>
    /// Gets the items related to a promotion.   
    /// </summary>
    /// <param name="promotionData">The promotion data to get items for.</param>
    /// <returns>
    /// The promotion condition and reward items.
    /// </returns>
    protected override PromotionItems GetPromotionItems(PercentagePromotionSample promotionData) {
      var specificItems = new CatalogItemSelection(
        promotionData.Condition.Items,
        CatalogItemSelectionType.Specific,
        promotionData.Condition.MatchRecursive);
      return new PromotionItems(promotionData, specificItems, specificItems);
    }

    /// <summary>
    /// Verify that the current promotion can potentially be fulfilled.
    /// </summary>
    /// <remarks>
    /// This method is a fast pre-check that avoids more expensive operations.
    /// In this case that a positive discount percentage has been set, and that the cart is not empty.
    /// </remarks>
    /// <param name="promotionData">The promotion to evaluate.</param>
    /// <param name="context">The context for the promotion processor evaluation.</param>
    /// <returns>
    /// <c>true</c> if the current promotion can potentially be fulfilled; otherwise, <c>false</c>.
    /// </returns>
    protected override bool CanBeFulfilled(PercentagePromotionSample promotionData, PromotionProcessorContext context) {
      if (promotionData.PercentageDiscount <= 0) {
        return false;
      }
      var lineItems = GetLineItems(context.OrderForm);
      if (!lineItems.Any()) {
        return false;
      }
      return true;
    }

    /// <summary>
    /// Gets all <see cref="AffectedItem"/>s affected by a given promotion.
    /// </summary>
    /// <param name="affectedItems">A collection of <see cref="PriceEntry"/>s to be checked against a promotion.</param>
    /// <param name="promotion">The promotion used to evaluate the line items.</param>
    /// <returns>A list of applicable <see cref="RedemptionDescription"/>s</returns>
    private IEnumerable < RedemptionDescription > GetRedemptions(IEnumerable < string > applicableCodes, PercentagePromotionSample promotionData, PromotionProcessorContext context) {
      var redemptions = new List < RedemptionDescription > ();
      var requiredQuantity = promotionData.Condition.RequiredQuantity;
      var maxRedemptions = GetMaxRedemptions(promotionData.RedemptionLimits);
      for (int i = 0; i < maxRedemptions; i++) {
        var affectedEntries = context.EntryPrices.ExtractEntries(applicableCodes, requiredQuantity);
        if (affectedEntries == null) {
          break;
        }
        redemptions.Add(CreateRedemptionDescription(affectedEntries));
      }
      return redemptions;
    }
  }
  #endregion
}