The quirks of coupons

If you have been using the promotion system in Episerver Commerce (or should I call it “old promotion system” – the “new promotion system” is almost out of BETA and it will soon be the promotion system), you should know about the coupon – which is an option setting for a promotion. When it is set, the customers will be required to input that special code in order to get the reward, even if their carts fulfilled all other requirements (the subtotal, the lineitems, the shipping method etc.)

The coupon in old promotion system is quite basic – you can set only one per promotion, or nothing at all. Advanced scenarios like customer-specific coupons (there are multiple coupons and each of them is valid for only one customer) are not supported. (This is one of reasons why you should consider to move to new promotion system ASAP). You can use redemption limits in combination to specify how many times a coupon can be used. But in this post we will talk about the quirks of storing it.

The coupon itself, is not part of the cart – but is considered as a part of the “context”. So when a customer adds a coupon to his/her cart, your actual code will look like this:

Snippet

MarketingContext.Current.AddCouponToMarketingContext(couponCode);

The coupon is now stored in MarketingProfileContext, and it’s where the quirks begin. MarketingProfileContext is stored in local thread storage if there is no HttpContext available, or in HttpContext.Current.Items if there is. So for most of the cases, it will be stored in HttpContext.Current.Items. When certain workflows (which call to CalculateDiscountsActivity) run MarketingContext will try to get the coupon from MarketingProfileContext and apply the promotion.

So what’s wrong? HttpContext.Current.Items is per-request storage. Next request, it will be lost. So if your customer wants to add a new item into the cart, so if he/she simply refreshs the cart page, or changes the address (which in most of the case will send new request to server) – then the storage – and therefore the coupon, is lost. The customer will have to input it again, or, you will have to store it somewhere.

There are two obvious options: Sessions and Cookies. Sessions are more convenient to use, as, in the end, it’s only a NameValueCollection where you can simply add and get back anything you like. However, Sessions can’t work in a load balancing environment. If you want to use load balancer, then Cookies are the only choice.

Here’s how you can do for the Cart page:

private void ApplyCoupons()
{
    var cookie = _cookieService.Get(CouponKey);
    if (cookie != null)
    {
        var coupons = JsonConvert.DeserializeObject<HashSet<string>>(cookie);
        if (coupons != null && coupons.Any())
        {
            foreach (var coupon in coupons)
            {
                MarketingContext.Current.AddCouponToMarketingContext(coupon);
            }
        }
    }
}

_cookieService is only simple service for getting the cookie from HttpContext.Current.Request. The reason of using HashSet<string> is to allow customers to input multiple coupon codes, but they must be unique. And to avoid problem with any special characters in coupon codes, here we use JsonConvert.DeserializeObject to form a string from the coupons.

But if you are certain that your coupons will not contain some special characters, a String.Join might be enough . And then you need to add the coupon when customer add it:

var cookie = _cookieService.Get(CouponKey);
var coupons = cookie != null ? JsonConvert.DeserializeObject<HashSet<string>>(cookie) : new HashSet<string>();
coupons.Add(couponCode);
_cookieService.Set(CouponKey, JsonConvert.SerializeObject(coupons));

It’s a bit tricky here – normally you would only want to save the valid coupons, so you might need to check first – by adding the coupon to the MarketingProfileContext and run certain workflows to see if it’s applied:

MarketingContext.Current.AddCouponToMarketingContext(couponCode);
_cartService.RunWorkflow(OrderGroupWorkflowManager.CartValidateWorkflowName);
_cartService.SaveCart();
 
if (!GetAppliedDiscountsWithCode().Where(d => couponCode.Equals(d.DiscountCode)).Any())
{
    return new EmptyResult();
}

Where GetAppliedDiscountsWithCode a method to check if any of the Discount(s) in the cart contains the coupon.

And then remove it when the customer no longer wants it in the cart:

var cookie = _cookieService.Get(CouponKey);
if (cookie != null)
{
    var coupons = JsonConvert.DeserializeObject<HashSet<string>>(cookie);
    if (coupons != null && coupons.Contains(couponCode))
    {
        coupons.Remove(couponCode);
        _cookieService.Set(CouponKey, JsonConvert.SerializeObject(coupons));
    }
}

And finally remove it when customer placed the order. It’s a bit tricky here as removing the cookie is not enough – you have to make it expired

if (HttpContext.Current != null)
{
    var httpCookie = new HttpCookie(cookie)
    {
        Expires = DateTime.Now.AddDays(-1)
    };
 
    HttpContext.Current.Response.Cookies.Add(httpCookie);
}

These quirks are solved in the new promotion system – each OrderForm now has its own collection of coupons – and it will be carried anywhere until the customer checks out the order.

One more reason to switch to the new promotion system?

Leave a Reply

Your email address will not be published. Required fields are marked *