Unique voucher/coupon codes in Episerver Commerce using Episerver Campaign
Oct 30, 2017
Using Episerver Commerce it’s possible to offer discounts to customers using the built in discount engine. Discounts can also have a coupon code associated meaning only customers who know that code can get the discount. However, it’s not possible to assign individual coupon codes for customers using Episerver Commerce alone. On the other hand Episerver Campaign can create, allocate and distribute unique coupon codes to customers.
So this post describes how it's possible to use Episerver Commerce and Episerver Campaign to validate and use individual coupon discount codes for customers.
The diagram below shows the interaction between the customer, Episerver Commerce and Episerver Campaign and describes what the code implements:
The steps below describe how it's possible to implement the unique voucher codes for customers using Episerver Commerce and Episerver Campaign.
Define the custom promotion type
First of all a custom promotion type is needed to allow Episerver Commerce users to select a coupon type that’s required in order to be eligible for the discount. The example below inherits one of the built in types (SpendAmountGetOrderDiscount). A new property is added to define the coupon code type defined in Episerver Campaign:
using System.ComponentModel.DataAnnotations; | |
using EPiServer.Commerce.Marketing; | |
using EPiServer.Commerce.Marketing.DataAnnotations; | |
using EPiServer.Commerce.Marketing.Promotions; | |
using EPiServer.DataAnnotations; | |
using EPiServer.Shell.ObjectEditing; | |
namespace EPiServer.Reference.Commerce.Site.Features.CampaignCoupons.Discount | |
{ | |
[ContentType(GUID = "F6B2B6EC-AA3F-415D-8802-C3FE18DA74A8" | |
, DisplayName = "Get a discount using a unique coupon code created by Episerver Campaign" | |
, Description = "Used to give discounts for individual coupon codes that have been sent to the user by Episerver Campaign")] | |
[AvailableContentTypes(Include = new[] {typeof(PromotionData)})] | |
[ImageUrl("~/Features/CampaignCoupons/campaign.png")] | |
public class SpendAmountGetOrderDiscountWithCoupon : SpendAmountGetOrderDiscount, ICampaignCouponDiscount | |
{ | |
// Hide the default coupon code | |
[ScaffoldColumn(false)] | |
public override CouponData Coupon { get; set; } | |
[PromotionRegion(PromotionRegionName.Condition)] | |
[SelectOne(SelectionFactoryType = typeof(CouponCodeTypesSelectionFactory))] | |
[Display( | |
Order = 1, | |
Name = "Coupon of this type has been sent to the user")] | |
public virtual string CouponType { get; set; } | |
} | |
} |
This new property gives users the ability so select from the available coupon codes that have been defined in Episerver Campaign:
It should be noted that there is some plumbing to connect the Episerver Campaign APIs to check for coupon types. Specifically the Episerver Campaign CouponBlockWebservice API is used in the CouponCodeTypesSelectionFactory to retrieve a list of coupon code types from Episerver Campaign and display them in the discount definition in Episerver Commerce. Full code can be found on Github and Gist as described in the Code section at the end of this post.
Coupon filter (ICouponFilter)
A ICouponFilter implementation is also required to check if the coupon code matches one that has been assigned to the user and also is the correct type for the discount. This uses some more of the Campaign APIs to ensure the coupon is valid and has been assigned to the current user, specifically the Episerver Campaign CouponCodeWebservice API:
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Campaign.CouponServices.Interfaces; | |
using EPiServer.Commerce.Marketing; | |
using EPiServer.Reference.Commerce.Site.Features.CampaignCoupons.Discount; | |
using EPiServer.Security; | |
using Mediachase.Commerce.Security; | |
namespace EPiServer.Reference.Commerce.Site.Features.CampaignCoupons.CouponFilter | |
{ | |
/// <summary> | |
/// Used to filter out promotions that use Episerver Campaign to issue coupon codes | |
/// </summary> | |
public class CampaignCouponFilter : ICouponFilter | |
{ | |
private readonly ICampaignCoupons _campaignCoupons; | |
public CampaignCouponFilter(ICampaignCoupons campaignCoupons) | |
{ | |
_campaignCoupons = campaignCoupons; | |
} | |
public PromotionFilterContext Filter(PromotionFilterContext filterContext, IEnumerable<string> couponCodes) | |
{ | |
var codes = couponCodes.ToList(); | |
var userEmail = PrincipalInfo.CurrentPrincipal?.GetCustomerContact()?.Email; | |
foreach (var includedPromotion in filterContext.IncludedPromotions) | |
{ | |
// Check if this is the right promotion type, if not ignore | |
var couponDrivenDiscount = includedPromotion as ICampaignCouponDiscount; | |
if (couponDrivenDiscount == null) continue; | |
// If we don't have any codes to check or the users email then unique | |
// coupon code promotion types are not valid so exclude them | |
if (!codes.Any() || string.IsNullOrEmpty(userEmail)) | |
{ | |
filterContext.ExcludePromotion(includedPromotion, FulfillmentStatus.CouponCodeRequired, | |
filterContext.RequestedStatuses.HasFlag(RequestFulfillmentStatus.NotFulfilled)); | |
continue; | |
} | |
long couponBlockId; | |
if (long.TryParse(couponDrivenDiscount.CouponType, out couponBlockId)) | |
{ | |
foreach (var couponCode in codes) | |
{ | |
// Check if the code its assigned to the user and that has not been used | |
if (_campaignCoupons.IsCouponAssigned(userEmail, couponBlockId, couponCode) && | |
_campaignCoupons.IsCouponUsed(couponBlockId, couponCode) == false) | |
{ | |
// The code hasn't been used and is assigned to the user so we can | |
// allow this to be connected to the promotion for this user | |
filterContext.AddCouponCode(includedPromotion.ContentGuid, couponCode); | |
} | |
else | |
{ | |
// Exclude this promotion as its been used and/or it isn't assigned to the user | |
filterContext.ExcludePromotion(includedPromotion, FulfillmentStatus.CouponCodeRequired, | |
filterContext.RequestedStatuses.HasFlag(RequestFulfillmentStatus.NotFulfilled)); | |
} | |
} | |
} | |
} | |
return filterContext; | |
} | |
protected virtual IEqualityComparer<string> GetCodeEqualityComparer() | |
{ | |
return StringComparer.OrdinalIgnoreCase; | |
} | |
} | |
} |
Marking the coupon as used (ICouponUsage)
When the customer checks out and completes the order the coupon should be marked as used to ensure the customer cannot get the same discount. We can use the ICouponUsage interface for this:
using System.Collections.Generic; | |
using Campaign.CouponServices.Interfaces; | |
using EPiServer.Commerce.Marketing; | |
using EPiServer.Core; | |
using EPiServer.Reference.Commerce.Site.Features.CampaignCoupons.Discount; | |
namespace EPiServer.Reference.Commerce.Site.Features.CampaignCoupons.Reporting | |
{ | |
public class CouponUsageImpl : ICouponUsage | |
{ | |
private readonly ICampaignCoupons _campaignCoupons; | |
private readonly IContentRepository _contentRepository; | |
public CouponUsageImpl(IContentRepository contentRepository, ICampaignCoupons campaignCoupons) | |
{ | |
_contentRepository = contentRepository; | |
_campaignCoupons = campaignCoupons; | |
} | |
public void Report(IEnumerable<PromotionInformation> appliedPromotions) | |
{ | |
// This method allows us to report couple usage so we | |
// will look for all promotions with a coupon applied | |
foreach (var promotion in appliedPromotions) | |
{ | |
var content = _contentRepository.Get<IContent>(promotion.PromotionGuid); | |
var promotionData = content as SpendAmountGetOrderDiscountWithCoupon; | |
long couponTypeId; | |
if (promotionData == null || long.TryParse(promotionData.CouponType, out couponTypeId) == false) return; | |
// Safe to mark the coupon as used | |
_campaignCoupons.MarkCouponAsUsed(couponTypeId, promotion.CouponCode); | |
} | |
} | |
} | |
} |
This simply marks the coupon as used so the customer cannot use it again.
Outcome
The outcome means that Episerver Campaign can distribute emails, SMS’s or even push notifications to customers containing unique coupon codes for those customers. Customers can then use the coupon code assigned to them to achieve a discount in their basket:
Conclusion
This post was written to share the code and approach that was demonstrated at the Episerver Partner Close up events in Stockholm and Helsinki. However it does show how easy it is to implement powerful features using the Episerver platform with little implementation effort. Some thought around caching would be advisable for production scenarios to reduce the dependencies on the Episerver Campaign APIs during promotion and price calculations.
All code was written on top of a standard Quicksilver site.
Code
The wrapper for the coupon API’s can be found over on Github and the code for the promotion can be found on Gist.
References / Useful links
- Episerver Campaign
- Episerver Campaign (web help)
- EPiserver Campaign Coupon Code API
- Episerver Campaign Coupon Block Definition API
- Episerver Custom promotion implementations
- Episerver Commerce ICouponFilter and ICouponUsage
- Note a bug has been raised about ICouponUsage.Report not firing. This has been been fixed in Commerce 11.4.0 onwards
- Campaign Coupon API wrapper
- Custom Promotion code