EPiServer Marketing [Beta] - creating custom order promotion

Lately, I had to build a custom order promotion in one of our projects in Geta. We are using latest EPiServer Marketing features but unfortunately as it is in Beta still, its API changes quite often. I had to re-build my custom promotion already twice. In this article, I will describe how to build a custom order promotion with the latest EPiServer Commerce version (9.15.0).

In the new version of EPiServer Marketing, promotion is just IContent. It can be loaded with IContentLoader, modified with IContentRepository etc.. There are several types of promotions. Below I defined simple OrderPromotion which applies a discount to the whole order.

[ContentType(
        DisplayName = "Additional Discount promotion",
        GUID = "E6271950-DB98-4FE6-9626-CEFCBF46BE19")]
public class AdditionalDiscountPromoData : OrderPromotion
{
}

There are also other types of promotions - EntryPromotion and ShippingPromotion.

Promotions are handled with promotion processors. For custom promotion, new promotion processor which inherits from PromotionProcessorBase should be implemented.

public class AdditionalDiscountPromoProcessor : PromotionProcessorBase<AdditionalDiscountPromoData>
{
    protected override RewardDescription Evaluate(
        AdditionalDiscountPromoData promotionData,
        PromotionProcessorContext context)
    {
        var orderForm = context.OrderForm;
        var cart = context.OrderGroup as Cart;
        if (cart == null)
        {
            return NoReward(promotionData);
        }

        var additionalDiscountPercent = (decimal)(cart[Constants.AdditionalDiscountPercentMetaField] ?? 0.0m);
        if (additionalDiscountPercent == 0)
        {
            return NoReward(promotionData);
        }

        return RewardDescription.CreatePercentageReward(
            FulfillmentStatus.Fulfilled,
            new[] {new RedemptionDescription(new AffectedOrder(orderForm)) },
            promotionData,
            additionalDiscountPercent,
            description: $"{additionalDiscountPercent} % discount applied to order");
    }

    private RewardDescription NoReward(PromotionData promotionData)
    {
        return new RewardDescription(
                FulfillmentStatus.NotFulfilled,
                Enumerable.Empty<RedemptionDescription>(),
                promotionData,
                unitDiscount: 0,
                unitPercentage: 0,
                rewardType: RewardType.None,
                description: "No discount applied");
    }

    protected override PromotionItems GetPromotionItems(AdditionalDiscountPromoData promotionData)
    {
        return new PromotionItems(
            promotionData,
            new CatalogItemSelection(null, CatalogItemSelectionType.All, true),
            new CatalogItemSelection(null, CatalogItemSelectionType.All, true));
    }
}

Two methods should be implemented - Evaluate and GetPromotionItems.

Evaluate is used to calculate what promotion should be or should not be applied. In the example above I am checking if AdditionalDiscountPercentMetaField is defined on the Cart. If it is not defined, I return RewardDescription with status NotFulfilled. So no discount will be applied. But if it has discount value, Fulfilled RewardDescription gets returned.

When creating RewardDescription correct sequence of RedemptionDescription should be provided. For OrderPromotion RedemptionDescription should take AffectedOrder parameter in the constructor. Other promotion types should use other Affected* types - AffectedItem for EntryPromotion and AffectedShipment for ShippingPromotion. These two should be used only for items which have discount. The previous version of Marketing required AffectedItem to be used for each line item in the order even if you used OrderPromotion.

Another method which should be implemented is GetPromotionItems - this is a new method. As I understand, it defines a query to look up for items to which discount might be applied. I do not know exactly how to build these queries but in the provided example all items are defined as valid for promotion.

Summary

It is really nice how EPiServer simplified and improved promotion creation. But some parts of an API is quite hard to understand without documentation. For example, a relation between promotion types and Affected* types. API should guide developer to use correct types.

I think that GetPromotionItems should not be forced on but have a default implementation. 90% of the time all items will apply for promotion. Also, building of queries should have a fluent interface so that API is easier to discover and use.