Simple restocked emails for Episerver Commerce

Frederik Vig 20-3-2019 20:18:05

We're going to add this to the Quicksilver reference site by Episerver. First thing we're going to do is update the product view to include a simple form that we'll show to the customer when a product is out of stock. 

This is for Product/Index.cshtml:

else if (!Model.IsInStock && !(bool) ViewData["IsReadOnly"])
{
   <p>Sold out</p>
    <p><a href="#sold-out-form" onclick="toggle_visibility('sold-out-form');">Email when available</a></p>
    <form id="sold-out-form" hidden action="@Url.Action("RestockNotification", "Warehouse")" method="post">
    @Html.Hidden("code", Model.Variant.Code)
    @Html.Hidden("warehouse", Model.Warehouse)
    @Html.AntiForgeryToken()
    <input type="email" name="email" placeholder="Email address" />
    <button type="submit">Email when available</button>
    <p>You will receive a one time email when the product becomes available. We won't share your mail address with anyone.</p>
    </form>
}

The form is simple that and will only be displayed when the customer clicks the  "Email when available" link. We could expand on this and include things like a quantity field and an add to mailing list checkbox.

In the WarehouseController.cs we'll add the code to handle the form data:

[HttpPost]
[AllowDBWrite]
[ValidateAntiForgeryToken]
public ActionResult RestockNotification(string code, string email, string warehouse)
{
   if (!ModelState.IsValid)
    {
       // validation etc
    }
    var restockNotificationEntry = new RestockedNotificationEntry
    {
    AddedUtc = DateTime.UtcNow,
       ModifiedUtc = DateTime.UtcNow,
       Code = code,
       Email = email,
       Warehouse = warehouse,
    };
    _applicationDbContext.RestockedNotifications.Add(restockNotificationEntry);
    _applicationDbContext.SaveChanges();
    return Json("We'll notify you as soon as we get restocked!");
}

 The code for storing the notification record in the database is also very simple and just uses EntityFramework.

public class ApplicationDbContext : DbContext
{
    private const string DatabaseConnectionName = "EPiServerDB";
    public ApplicationDbContext() : base(DatabaseConnectionName)
   {
   }
    public DbSet<RestockedNotificationEntry> RestockedNotifications { get; set; }
}
public class RestockedNotificationEntry
{
   [Key]
    public int Id { get; set; }
    public string Code { get; set; }
    public string Email { get; set; }
    public DateTime AddedUtc { get; set; }
    public DateTime ModifiedUtc { get; set; }
    public bool IsSent { get; set; }
    public string Warehouse { get; set; }
}

The main thing left now is creating a scheduled job that will send out the notifications.

using System;
using System.Data.Entity;
using System.Linq;
using System.Net.Mail;
using System.Text;
using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.PlugIn;
using EPiServer.Reference.Commerce.Shared.Services;
using EPiServer.Scheduler;
using Mediachase.Commerce.Catalog;
using Mediachase.Commerce.InventoryService;
namespace EPiServer.Reference.Commerce.Site.Features.Warehouse
{
    [ScheduledPlugIn(DisplayName = "Notification Restock", GUID = "1DBDEC06-8735-4033-A9E5-FC7822FA32C9")]
public class RestockNotificationsJob : ScheduledJobBase
{
private bool _stopSignaled;
private readonly ApplicationDbContext _applicationDbContext;
private readonly IInventoryService _inventoryService;
private readonly IMailService _mailService;
private readonly IContentLoader _contentLoader;
private readonly ReferenceConverter _referenceConverter;
/// <summary>
/// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
/// </summary>
public override void Stop()
{
_stopSignaled = true;
}
public RestockNotificationsJob(ApplicationDbContext applicationDbContext, IInventoryService inventoryService, IMailService mailService, IContentLoader contentLoader, ReferenceConverter referenceConverter)
{
IsStoppable = true;
_applicationDbContext = applicationDbContext;
_inventoryService = inventoryService;
_mailService = mailService;
_contentLoader = contentLoader;
_referenceConverter = referenceConverter;
}
public override string Execute()
{
OnStatusChanged($"Starting execution of {this.GetType()}");
int count = 0;
// two ways of going about it - listening to inventory change events or traversing through all in DB
var allNotificationEntries = _applicationDbContext.RestockedNotifications.ToList();
var groupedByCode = allNotificationEntries.GroupBy(n => n.Code);
foreach (var entries in groupedByCode)
{
foreach (var warehouseEntries in entries.GroupBy(m => m.Warehouse))
{
var inventory = _inventoryService.Get(entries.Key, warehouseEntries.Key);
if (inventory == null)
{
continue;
}
if (inventory.PurchaseAvailableQuantity > 0 && inventory.PurchaseAvailableUtc < DateTime.UtcNow)
{
foreach (var restockedNotificationEntry in warehouseEntries)
{
if (_stopSignaled)
{
return "Stop of job was called";
}
if (restockedNotificationEntry.IsSent)
{
continue;
}
var product = _contentLoader.Get<EntryContentBase>(_referenceConverter.GetContentLink(restockedNotificationEntry.Code));
string subject = $"{product.DisplayName} is now back in stock! MyStore.com";
var body = new StringBuilder();
body.Append($"<h1>{product.DisplayName}</h1>");
body.Append($"<p>You asked to tell you when {product.DisplayName} is back in stock.</p>");
body.Append($"<p>We’re pleased to tell you it’s available for purchase. Click here to place your order. (include image and some details).</p>");
var mailMessage = new MailMessage(from: "[email protected]", to: restockedNotificationEntry.Email, subject: subject, body: body.ToString())
{
IsBodyHtml = true
};
_mailService.Send(mailMessage);
restockedNotificationEntry.IsSent = true;
restockedNotificationEntry.ModifiedUtc = DateTime.UtcNow;
_applicationDbContext.SaveChanges();
count++;
}
}
}
}
int removedCount = 0;<
foreach (var restockedNotificationEntry in allNotificationEntries)
{
if (restockedNotificationEntry.IsSent)
{
DateTime lastModified = restockedNotificationEntry.ModifiedUtc;
DateTime now = DateTime.UtcNow;
TimeSpan ts = now - lastModified;
int days = Math.Abs(ts.Days);
if (days >= 30) // delete sent entries that are older than 30 days.
{
_applicationDbContext.Entry(restockedNotificationEntry).State = EntityState.Deleted;
_applicationDbContext.SaveChanges();
removedCount++;
}
}
}
// good to create report over who wants certain items etc
return $"Sent {count} notifications. Removed {removedCount} old notifications.";
}
}
}

Note that the email code is very simple and just an example. There are of course a lot of different libraries and tools out there to help with this. We usually use our own: Geta Email Notification. The email and the links can be tracked using your favorite tracking service. It's important that we don't spam the customer and only send the email once. 

We're also automatically removing old records that are 30 or more days old (and that have been sent). Bonus would of course be to create a report using this data or sending it to an analytics service. That way it's easier to see which products should be restocked first.