Email Marketing Coupons module
Have you ever wondered how to automate distribution of discount coupons to customers? Recently we have published a module on our Marketplace that allows you to do just this. In this article you can find out how we approached the design and implementation of this module.
Module requirements
One of the questions that started to come up since the introduction of the Order discounts, Free shipping offers and Buy X Get Y discounts was how it is possible to automate distribution of discount coupons to customers. The module I have chosen to implement shows one approach to this scenario, and that is using the Email marketing module.
As general custom module development has already been well covered in the Kentico documentation, this article will only concentrate on a few specific points encountered when designing and developing this module.
Before starting the development, I have considered what could eventually be expected from the planned functionality. Here are the most important requirements the module should meet:
- Campaign administrator can repeatedly resend the same coupon set in multiple Emails.
- The same Subscriber gets the same coupon if the same promotion is resent in multiple Emails.
- Single Email may be used in more than one coupon promotion.
- Single promotion may include coupons from different types of Kentico discounts.
- Subscriber coupon assignments can be used outside the Email marketing module.
User interface
Before we discuss the implementation in more detail, let’s see the final module UI first, so that you can imagine the functionality.
The module assigns coupons to individual Subscriber email addresses. The assigned coupons are grouped in a Coupon batch, which represents a single coupon promotion. Code name of this batch is then used to look up the coupon belonging to Subscriber’s email. To simplify the batch management, each Coupon batch belongs to a specific email campaign, and can be edited by a newly added UI element in the Email campaign properties:
Each Subscriber email address can be present in the batch only once, but a single coupon can be shared by multiple email addresses:
This can be useful if you want to send different discounts to different Subscribers in the same batch. For example, you can assign a single-use Order discount coupon to all Subscribers that have subscribed to the Newsletter by today. And later you can assign another shared Product coupon with a lower value to any new Subscribers that have subscribed between now and when the campaign Email is to be sent out.
This assignment is based on parameters that can be specified in the Add coupons tab. The tab allows you to select the type of discount that you want to add, and then select an object of given type from Kentico. The Custom value option also allows you to enter the coupon manually:
Different discounts will result in different behavior when assigning discount to Subscribers. Product coupon and custom value options will make the module assign the same coupon to all selected Subscribers, while selecting an Order discount, Buy X Get Y discount or Free shipping offer option assigns each of the selected Subscribers with a unique Coupon code associated with that discount. When assigning these three types of coupons, you can also make use of the Coupon code prefix field to filter which coupons are used. This field is also used when you decide to enable the Coupon code generator to generate coupons for Subscribers, should the existing ones not be sufficient.
After selecting the discount parameters, the module assigns coupons to the selected Subscribers:
Afterwards the coupon can be retrieved in campaign’s Email, using the macro which takes the batch code name and Subscriber email address as parameters:
The coupon is displayed after the email has been sent out to Subscribers:
Project structure
The implementation of the module consists of several different parts. Apart from the UI, which has been defined as part of a custom Email marketing coupons module, the module consists of several classes located in ~/App_Code/Modules/EmailMarketingCoupons. Later, we will look at some of these in more detail:
- CouponSubscriberInfo.cs and CouponSubscriberInfoProvider.cs
- Code generated by the custom class in Email marketing coupons module.
- Instances of this class define the Subscriber coupon mapping
- CouponBatchInfo.cs and CouponBatchInfoProvider.cs
- Code generated by the custom class in Email marketing coupons module.
- Groups multiple CouponSubscriberInfo entries into a single Coupon batch
- CouponCodeGenerator.cs
- Contains a standalone Coupon code generator implementation
- CouponSubscriberMacros.cs
- Contains a macro class used to display macros in emails
- NewsletterInfoExtender.cs
- Utility extender class, containing code used to retrieve Newsletter’s Subscribers
- EmailMarketingCoupons.cs and EmailMarketingCouponsMetadata.cs
- Classes containing additional module information
UI listings in the module use the UniGrid control. The XML definition of these UniGrids, following the standard UniGrid xml syntax, can be found in:
~/App_Data/CMSModules/EmailMarketingCoupons/UI/Grids/
The Add coupons UI element uses a custom control, which can be found in:
~/CMSModules/EmailMarketingCoupons/Pages/Tools/Add_Coupons.aspx
Finally, the module includes a file with resource strings used by its controls and UI:
~/CMSResources/EmailMarketingcoupons/Common.resx
Implementation
The majority of development of this module was done in CouponCodeGenerator.cs, NewsletterInfoExtender.cs and Add_Coupons.aspx.cs, which we will now take a closer look at.
The Add_Coupons control is the center point of the module logic. It renders UI for discount and Subscriber selection, and handles assignment of coupons to Subscribers. The assignment of coupons is carried out asynchronously by the AsyncLog control, while showing output log of actions to the Administrator.
The Subscriber selection UniGrid has more logic to it than it may appear at first glance. Kentico does not have just one method of subscribing to Email marketing campaigns. A User, Role, Contact, Contact group or Persona can be all subscribed to email campaigns. This means that information about each of these needs to be collected and merged into a single DataSet, before it can be rendered by the UniGrid.
For this purpose, I have implemented the NewsletterInfoExtender extender class with the GetSubscribers method. This method processes the NewsletterInfo class, which represents the Email campaign, and returns a unified DataSet as a result. This DataSet is then used for both populating the Subscriber selection UniGrid and assigning coupons.
If you would like to know how the method works, I strongly recommend taking a look at the complete code, but here are the most important parts.
NewsletterInfoExtender.cs
The method initializes a new DataSet, and then adds data from all Subscriber types:
// Init subscribers table
DataSet ds = new DataSet();
ds.Tables.Add();
ds.Tables[0].Columns.Add("SubscriberEmail", typeof(string));
ds.Tables[0].Columns.Add("SubscriberFirstName", typeof(string));
ds.Tables[0].Columns.Add("SubscriberLastName", typeof(string));
ds.Tables[0].Columns.Add("SubscriberName", typeof(string));
ds.Tables[0].Columns.Add("SubscriberType", typeof(string));
// Get standard and Contact subscribers. Their email is included in the SubscriberInfo record directly.
DataSet basicSubscribers = Newsletter.GetSubscribersQuery(IncludeBounced, OnlyApproved)
.Columns("SubscriberEmail", "SubscriberFirstName", "SubscriberLastName", "SubscriberType")
.WhereNotNull("SubscriberEmail");
...
// Get Users
var userSubscribers = Newsletter.GetSubscribersQuery(IncludeBounced, OnlyApproved)
.Column("SubscriberRelatedID")
.WhereEquals("SubscriberType", UserInfo.OBJECT_TYPE);
var queryUsers = UserInfoProvider.GetUsers()
.Columns("Email AS SubscriberEmail", "FirstName AS SubscriberFirstName", "LastName AS SubscriberLastName", "Username AS SubscriberName")
.WhereIn("UserID", userSubscribers);
if (!DataHelper.DataSourceIsEmpty(queryUsers))
{
// Add subsriber type column
queryUsers.Tables[0].Columns.Add("SubscriberType", typeof(string), "'" + UserInfo.OBJECT_TYPE + "'");
// Add to result dataset
ds.Tables[0].Merge(queryUsers.Tables[0]);
}
...
Add_Coupons.aspx.cs
After discount and Subscribers have been selected, and the Administrator has clicked the “Assign” button, the selected UI parameters are copied over to the related instance variables, and used by the asynchronous AssignCodes method that is run by the AsyncLog. This has to be done to assure persistence of the data for the multithreaded logic. The asynchronous method then collects a list of all available discount coupons belonging to the selected discount, if necessary generating additional coupons, and merges the coupon and Subscriber data. The result DataTable is then inserted to the database using ConnectionHelper.BulkInsert as CouponSubscriberInfo class entries:
BulkInsertSettings settings = new BulkInsertSettings
{
Mappings = new Dictionary<string, string>
{
{"SubscriberCouponCode", "CouponSubscriberCouponCode"},
{"SubscriberEmail", "CouponSubscriberEmail"},
{"SubscriberFirstName", "CouponSubscriberFirstName"},
{"SubscriberLastName", "CouponSubscriberLastName"},
{"SubscriberType", "CouponSubscriberType"},
{"SubscriberName", "CouponSubscriberName"},
{"SubscriberCouponBatchID", "CouponSubscriberCouponBatchID"}
}
};
try
{
ConnectionHelper.BulkInsert(subscribers, CouponSubscriberInfo.TYPEINFO.ClassStructureInfo.TableName, settings);
}
catch (Exception ex)
{
LogAndShowError("EmailMarketingCoupons", "INSERTFAILED", ex);
}
The BulkInsert feature is useful for creating and inserting a large set of simple objects, like the CouponSubscriberInfo. It provides better performance than casting DataTable rows to CouponSubscriberInfo objects and inserting them one by one using a standard API method does. This way the module can process a large amount of Subscribers.
CouponSubscriberMacros.cs
With the coupon data saved in the database, the last step is placing the GetSubscriberCoupon macro method into an Email. This macro takes the Coupon batch code name and the Subscriber email as parameters and returns the appropriate coupon.
The custom macro method code can be found in the CouponSubscriberMacros.cs file. The method itself is simple. It verifies that the parameters are not empty, and loads the batch with the given name:
// Get batch information
Func<InfoDataSet<CouponBatchInfo>> getBatch = () =>
CouponBatchInfoProvider.GetCouponBatches()
.Source(sourceItem => sourceItem.Join<NewsletterInfo>("CouponBatchNewsletterID", "NewsletterID"))
.WhereEquals("CouponBatchName", batchName)
.WhereEquals("NewsletterSiteID", site.SiteID)
.Column("CouponBatchID")
.TopN(1)
.TypedResult;
CacheSettings csBatch = new CacheSettings(cacheMinutes, (batchName + site.SiteName).ToLower())
{
CacheDependency = CacheHelper.GetCacheDependency(CouponBatchInfo.TYPEINFO.ObjectClassName.ToLower() + "|byname|" + batchName)
};
var batch = CacheHelper.Cache(getBatch, csBatch).FirstOrDefault();
Afterwards, the macro method retrieves a set of all coupons from the Coupon batch, and saves it to the cache for further use. This way the macro only queries the database the first time it is run for any given Coupon batch, and then takes data from the cache:
// Prepare query to retrieve all subscribers in batch for caching
Func<InfoDataSet<CouponSubscriberInfo>> GetCodes = () =>
CouponSubscriberInfoProvider.GetCouponSubscribers()
.WhereEquals("CouponSubscriberCouponBatchID", batch.CouponBatchID)
.TypedResult;
// Prepare cache settings
int cacheMinutes = (parameters.Length > 2) ? ValidationHelper.GetInteger(parameters.Length, 10) : 10;
CacheSettings cs = new CacheSettings(cacheMinutes, batchName.ToLower() + "keys")
{
CacheDependency = CacheHelper.GetCacheDependency(CouponSubscriberInfo.TYPEINFO.ObjectClassName.ToLower() + "|all")
};
// Retrieve, and if necessary cache, all subscriber coupons from the given batch
var subscriberCoupons = CacheHelper.Cache(GetCodes, cs);
// Get coupon subscriber from the cached DataSet
var couponSubscriber = subscriberCoupons.FirstOrDefault(subscriber => subscriber.CouponSubscriberEmail == subscriberEmail);
if (couponSubscriber == null)
{
return string.Empty;
}
CouponCodeGenerator.cs
The last class worth mentioning is the generator class in CouponCodeGenerator.cs. This is a standalone reimplementation of the existing coupon generator from the E-commerce module. I have extracted the logic and implemented three simple-to-use members for it. This reduces the operation of the generator to these few lines of code:
// Use constructor to initialize the generator
CouponCodeGenerator generator = new CouponCodeGenerator(generatorDiscount, missingCoupons, mNumberOfUses, mPrefix);
// Optionally register for output message event
generator.OnMessageRaised += Generator_OnMessageRaised;
// Generate coupons
coupons.AddRange(generator.GenerateCouponCodes());
It can be useful for you if you need to generate Kentico E-commerce Coupon codes as part of your own custom logic.
As you can see, the Kentico API allows you to implement a very versatile module with just a few hundred lines of code, and provide a brand new functionality that is able to interact, collect, analyze and reuse the existing Kentico data.
I hope you found this article interesting and it will help you in developing your own amazing modules.