allBlogsList

Deployment Automation Custom Email Templates

Introduction

In our latest Optimizely Marketplace project - Tax Exempt Certificate Management, we created a new Email Template that would be sent to configured email recipients when a new Tax Exempt Certificate was submitted by an end-user. The challenge was to automate the setup of the new Email Template into any existing Optimizely B2B Commerce site without the need for custom implementation services. Normally, a new Email Template would be created inside the Admin Console. This involves a site administrator to define the layout of the Email Template source code using the Email Template editor built into Optimizely B2B Commerce. They would then publish the completed Email Template and associate it with an existing Email List. Not a trivial task for most Site Administrators. So how did we do it?

Optimizely B2B Commerce makes it possible to extend the existing BootStrapper process with custom Startup Tasks. During the BootStrapper process, the system retrieves all class modules that correctly implement the IStartupTask interface. It then sorts the Startup Tasks and executes them.

These Startup Tasks perform a variety of work such as configuring the OData services, initializing the AWS storage provider, creating system lists, and registerig site messages.

The IStartupTask Interface

The IStartupTask interface is simple in that it only defines one method that must be implemented, Run(). The two required properties will be provided to the method during execution by the BootStrapper process.

    public interface IStartupTask : IMultiInstanceDependency, IExtension
    {
        void Run(IAppBuilder app, HttpConfiguration config);
    }
    ** End Code Formatted Block **

Adding a new BootStrapper Task

Our solution has a good starting place to automatically create a published Email Template. We will create a new StartupTask that implements the IStartupTask interface. Here is a good starting class.

This new class should be created in the Extensions project of your solution.

    public class CustomStartupTask : IStartupTask
    {
        // TODO: Add required dependencies to constructor.
        public CustomStartupTask() { }

There is another gotcha in creating a custom Startup Task, we must also declare the BootStrapperOrder in the new class. This tells the BootStrapper the specific order to run the code modules. If the BootStrapperOrder attribute is missing or no value is provided, you will get an error during startup.

Here is the previous code with the BootStrapperOrder attribute:

    [BootStrapperOrder(11)]
    public class CustomStartupTask : IStartupTask
    {
        // TODO: Add required dependencies to constructor.
        public CustomStartupTask() { }

The next step is to add the specific functionality to automatically register our custom Email Template during startup.

    [BootStrapperOrder(11)]
    public class CustomStartupTask : IStartupTask
    {
        protected readonly IUnitOfWork UnitOfWork;
        protected readonly IPerRequestCacheManager PerRequestCacheManager;
        protected readonly IContentManagerUtilities ContentManagerUtilities;

        public CustomStartupTask(
            IUnitOfWorkFactory unitOfWorkFactory,
            IPerRequestCacheManager perRequestCacheManager,
            IContentManagerUtilities contentManagerUtilities
        ) 
        { 
            UnitOfWork = unitOfWorkFactory.GetUnitOfWork();
            PerRequestCacheManager = perRequestCacheManager;
            ContentManagerUtilities = contentManagerUtilities;
        }

        public void Run(IAppBuilder app, HttpConfiguration config)
        {
            var emailTemplateRepository = UnitOfWork.GetRepository<EmailTemplate>();

            var defaultEmailTemplateNames = new List<string>
            {
                "CustomEmailTemplate"
            };

            foreach (var emailTemplateName in defaultEmailTemplateNames)
            {
                var matchingEmailTemplate = emailTemplateRepository.GetTableAsNoTracking()
                    .FirstOrDefault(x => x.Name.equals(emailTemplateName));

                if (matchingEmailTemplate == null)
                {
                    var defaultLanguage = UnitOfWork.GetTypedRepository<ILanguageRepository>().GetDefault();
                    var defaultPersona = UnitOfWork.GetTypedRepository<IPersonaRepository>().GetDefault();

                    var content = new Content
                    {
                        Revision = 1,
                        SubmittedForApprovalOn = DateTimeProvider.Current.Now,
                        PublishToProductionOn = DateTimeProvider.Current.Now,
                        CreatedBy = null,
                        ApprovedOn = DateTimeProvider.Current.Now,
                        ApprovedByAdminUserProfile = null,
                        Language = defaultLanguage,
                        Persona = defaultPersona,
                        DeviceType = DeviceType.Desktop.ToString(),
                        Html = GetDefaultEmailTemplateContent(emailTemplateName)
                    };

                    var emailTemplate = new EmailTemplate
                    {
                        Name = emailName,
                        ContentManager = new ContentManager { Name = nameof(EmailTemplate) }
                    };

                    var contentManagerRepository = UnitOfWork.GetRepository<ContentManager>();
                    contentManagerRepository.Insert(emailTemplate.ContentManager);
                    emailTemplateRepository.Insert(emailTemplate);

                    var contentManager = emailTemplate.ContentManager;
                    ContentManagerUtilities.AddContent(contentManager, content);
                    UnitOfWork.Save();
                }
            }
        }
        
        private string GetDefaultEmailTemplateContent(string templateName)
        {
            var virtualPath = $"/Themes/XCentium/Views/DefaultEmails/{templateName}.cshtml";
            var physicalPath = PathProvider.Current.MapPath(virtualPath);
            if (File.Exists(physicalPath))
            {
                return File.ReadAllText(physicalPath);
            }

            throw new ArgumentException(
                $"The Email Template '{templateName}' had no approved revisions and there was no default template found at {virtualPath}"
            );
        }
    }    

Where do we store the Email Template?

The private method, GetDefaultEmailTemplateContent, is necessary because the source folder for out-of-the-box Email Templates exists in the _SystemResources folder. The system will not recognize any changes to the content in protected folders, ie. files can not be added, deleted, or modified. We can create a new folder to hold these custom resources and load these custom Email Templates from that location.

Summary

This article shows us how to hook into the BootStrapper process and add custom content to Optimizely B2B Commerce. This ensures proper configuration of our Marketplace products and helps ease the financial burden on the Customer by avoiding costly implementation contracts.

Thank you for your time, I hope you found this article useful. Feel free to reach out to me or XCentium directly if you have questions or interest in any of our custom solutions for Optimizely B2B Commerce. We would love to work with you to help maximize your investment in Optimizely B2B Commerce.