Thursday, February 22, 2018

Sitecore Experience Commerce - AddPriceSnapshot: Cannot read property 'find' of undefined

When testing out the postman services on my local Experience Commerce instance, I would get the following error when attempting to add a snapshot to a price card:
TypeError | Cannot read property 'find' of undefined
Postman console showing: Cannot read property 'find' of undefined
It turns out that the Postman example had the date in american format of mm/dd/yyyy where as my timezone is New Zealand, the expected format was dd/mm/yyyy. Changing the order of day and month allowed the AddPriceSnapshot API call to succeed.

Sitecore Experience Commerce - Adding a new environment

In Sitecore's Experience Commerce an environment can be used to separately configure data and functionality to create a standalone store. In my case, I would have two unique stores running on the same commerce instance (with no shared data or configurations), so would need two clean environments created.

For this example I will be configuring a new environment, which will contain an empty inventory set. In the Habitat and Adventure works examples that come out of the box, they take it further by adding products, catalogs, categories, price cards and promotions.

Creating a plugin

The first step is to open up the Customer.Sample.Solution which came with the SDK as part of the commerce install. This is the application which gets deployed to the four commerce sites (authoring, minions, ops and shops). You should ensure that the Sitecore.Commerce.Plugin.vsix visual studio plugin has been added. This will allow you to create a sample plugin, in my case Plugin.MyShop.Shop.

This is where we hook into the initialize environment pipeline to instantiate our environment/inventory set.

Many of the sample classes were not required and based on the Adventure Works example, I cut mine down to the following:

Experience Commerce plugin for environment initialization
The code used is as follows:

InitializeInventoryBlock
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InitializeInventoryBlock.cs" company="Sitecore Corporation">
//   Copyright (c) Sitecore Corporation 1999-2017
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Plugin.MyShop.Shop
{
    using System.IO;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Http.Internal;
    using Sitecore.Commerce.Core;
    using Sitecore.Commerce.Plugin.Catalog;
    using Sitecore.Framework.Pipelines;
    using Sitecore.Commerce.Plugin.Inventory;

    /// <summary>
    /// Ensure Habitat catalog has been loaded.
    /// </summary>
    /// <seealso>
    ///     <cref>
    ///         Sitecore.Framework.Pipelines.PipelineBlock{System.String, System.String,
    ///         Sitecore.Commerce.Core.CommercePipelineExecutionContext}
    ///     </cref>
    /// </seealso>
    [PipelineDisplayName(NwConstants.Pipelines.Blocks.InitializeCatalogBlock)]
    public class InitializeInventoryBlock : PipelineBlock<string, string, CommercePipelineExecutionContext>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="InitializeCatalogBlock"/> class.
        /// </summary>
        /// <param name="hostingEnvironment">The hosting environment.</param>
        /// <param name="importInventorySetsCommand">The import catalog command.</param>
        public InitializeInventoryBlock(
            IHostingEnvironment hostingEnvironment,
            ImportInventorySetsCommand importInventorySetsCommand)
        {
            this.HostingEnvironment = hostingEnvironment;
            this.ImportInventorySetsCommand = importInventorySetsCommand;
        }

        /// <summary>
        /// Gets the <see cref="IHostingEnvironment"/> implementation.
        /// </summary>
        protected IHostingEnvironment HostingEnvironment { get; }

        /// <summary>
        /// Gets the <see cref="ImportInventorySetsCommand"/> implementation.
        /// </summary>
        protected ImportInventorySetsCommand ImportInventorySetsCommand { get; }

        /// <summary>
        /// Executes the block.
        /// </summary>
        /// <param name="arg">The argument.</param>
        /// <param name="context">The context.</param>
        /// <returns></returns>
        public override async Task<string> Run(string arg, CommercePipelineExecutionContext context)
        {
            var artifactSet = "Environment.MyShop.Catalog-1.0";

            // Check if this environment has subscribed to this Artifact Set
            if (!context.GetPolicy<EnvironmentInitializationPolicy>().InitialArtifactSets.Contains(artifactSet))
            {
                return arg;
            }

            using (var stream = new FileStream(this.GetPath("MyShop_Inventory.zip"), FileMode.Open, FileAccess.Read))
            {
                var file = new FormFile(stream, 0, stream.Length, stream.Name, stream.Name);
                await this.ImportInventorySetsCommand.Process(context.CommerceContext, file, CatalogConstants.ImportMode.Replace, 10);
            }

            return arg;
        }

        private string GetPath(string fileName)
        {
            return Path.Combine(this.HostingEnvironment.WebRootPath, "data", "Catalogs", fileName);
        }
    }
}
RegisteredPluginBlock
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Plugin.MyShop.Shop.Pipelines.Blocks
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Sitecore.Commerce.Core;
    using Sitecore.Framework.Pipelines;

    /// <summary>
    /// Defines the registered plugin block.
    /// </summary>
    /// <seealso>
    ///     <cref>
    ///         Sitecore.Framework.Pipelines.PipelineBlock{System.Collections.Generic.IEnumerable{Sitecore.Commerce.Core.RegisteredPluginModel},
    ///         System.Collections.Generic.IEnumerable{Sitecore.Commerce.Core.RegisteredPluginModel},
    ///         Sitecore.Commerce.Core.CommercePipelineExecutionContext}
    ///     </cref>
    /// </seealso>
    [PipelineDisplayName(NwConstants.Pipelines.Blocks.RegisteredPluginBlock)]
    public class RegisteredPluginBlock : PipelineBlock<IEnumerable<RegisteredPluginModel>, IEnumerable<RegisteredPluginModel>, CommercePipelineExecutionContext>
    {
        /// <summary>
        /// The run.
        /// </summary>
        /// <param name="arg">
        /// The argument.
        /// </param>
        /// <param name="context">
        /// The context.
        /// </param>
        /// <returns>
        /// The list of <see cref="RegisteredPluginModel"/>
        /// </returns>
        public override Task<IEnumerable<RegisteredPluginModel>> Run(IEnumerable<RegisteredPluginModel> arg, CommercePipelineExecutionContext context)
        {
            if (arg == null)
            {
                return Task.FromResult((IEnumerable<RegisteredPluginModel>)null);
            }

            var plugins = arg.ToList();
            PluginHelper.RegisterPlugin(this, plugins);

            return Task.FromResult(plugins.AsEnumerable());
        }
    }
}
NwConstants
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Plugin.MyShop.Shop
{
    class NwConstants
    {
        /// <summary>
        /// The names of the my shop pipelines.
        /// </summary>
        public static class Pipelines
        {
            /// <summary>
            /// The names of the my shop pipelines blocks.
            /// </summary>
            public static class Blocks
            {
                /// <summary>
                /// The initialize catalog block name.
                /// </summary>
                public const string InitializeCatalogBlock = "MyShop.block.InitializeCatalog";

                /// <summary>
                /// The registered plugin block name.
                /// </summary>
                public const string RegisteredPluginBlock = "MyShop:block:registeredplugin";
            }
        }
    }
}
ConfigureSitecore
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ConfigureSitecore.cs" company="Sitecore Corporation">
//   Copyright (c) Sitecore Corporation 1999-2017
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Sitecore.Commerce.Plugin.Sample
{
    using System.Reflection;
    using global::Plugin.MyShop.Shop;
    using global::Plugin.MyShop.Shop.Pipelines.Blocks;
    using Microsoft.Extensions.DependencyInjection;
    using Sitecore.Commerce.Core;
    using Sitecore.Framework.Configuration;
    using Sitecore.Framework.Pipelines.Definitions.Extensions;

    /// <summary>
    /// The configure sitecore class.
    /// </summary>
    public class ConfigureSitecore : IConfigureSitecore
    {
        /// <summary>
        /// The configure services.
        /// </summary>
        /// <param name="services">
        /// The services.
        /// </param>
        public void ConfigureServices(IServiceCollection services)
        {
            var assembly = Assembly.GetExecutingAssembly();
            services.RegisterAllPipelineBlocks(assembly);

            services.Sitecore().Pipelines(config => config
             .ConfigurePipeline<IInitializeEnvironmentPipeline>(d =>
             {
                 d.Add<InitializeInventoryBlock>();
             })
             .ConfigurePipeline<IRunningPluginsPipeline>(c => { c.Add<RegisteredPluginBlock>().After<RunningPluginsBlock>(); }));

        }
    }
}
Now we add this project as a reference on the Sitecore.Commerce.Engine project.

The inventory zip

The code above imports an inventory set using a zip file (MyShop_Inventory.zip) contains JSON files which are used to define inventory sets, catalogs, relationships and so on. In this example I simply define an inventory set and localization entity. The JSON files are zipped up and placed into src\Sitecore.Commerce.Engine\wwwroot\data\Catalogs.

InventorySet.1.json
{
   "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Plugin.Inventory.InventorySet, Sitecore.Commerce.Plugin.Inventory]], mscorlib",
   "$values":[
      {
         "$type":"Sitecore.Commerce.Plugin.Inventory.InventorySet, Sitecore.Commerce.Plugin.Inventory",
         "Description":"This inventory catalog contains inventory for My Shop",
         "Components":{
            "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Component, Sitecore.Commerce.Core]], mscorlib",
            "$values":[
               {
                  "$type":"Sitecore.Commerce.Plugin.ManagedLists.ListMembershipsComponent, Sitecore.Commerce.Plugin.ManagedLists",
                  "Memberships":{
                     "$type":"System.Collections.Generic.List`1[[System.String, mscorlib]], mscorlib",
                     "$values":[
                        "InventorySets"
                     ]
                  },
                  "Id":"7dcc1f853eb54a90b62978bb721b8340",
                  "Name":"",
                  "Comments":"",
                  "Policies":{
                     "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Policy, Sitecore.Commerce.Core]], mscorlib",
                     "$values":[

                     ]
                  },
                  "ChildComponents":{
                     "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Component, Sitecore.Commerce.Core]], mscorlib",
                     "$values":[

                     ]
                  }
               },
               {
                  "$type":"Sitecore.Commerce.Core.LocalizedEntityComponent, Sitecore.Commerce.Core",
                  "Entity":{
                     "$type":"Sitecore.Commerce.Core.EntityReference, Sitecore.Commerce.Core",
                     "Name":"",
                     "EntityTarget":"Entity-LocalizationEntity-6df7b4690b944b1cb68c4364a3963e86",
                     "Policies":{
                        "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Policy, Sitecore.Commerce.Core]], mscorlib",
                        "$values":[

                        ]
                     }
                  },
                  "Id":"03f53c88e4ce4ca5b0016528d86f28f9",
                  "Name":"",
                  "Comments":"",
                  "Policies":{
                     "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Policy, Sitecore.Commerce.Core]], mscorlib",
                     "$values":[

                     ]
                  },
                  "ChildComponents":{
                     "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Component, Sitecore.Commerce.Core]], mscorlib",
                     "$values":[

                     ]
                  }
               }
            ]
         },
         "DateCreated":"2018-01-11T18:43:34.5143208+00:00",
         "DateUpdated":"2018-01-11T18:43:34.5293214+00:00",
         "DisplayName":"My Shop Inventory",
         "FriendlyId":"My Shop Inventory",
         "Id":"Entity-InventorySet-My Shop Inventory",
         "Version":1,
         "IsPersisted":true,
         "Name":"My Shop Inventory",
         "Policies":{
            "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Policy, Sitecore.Commerce.Core]], mscorlib",
            "$values":[

            ]
         }
      }
   ]
}
LocalizationEntity.1.json
{
   "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.LocalizationEntity, Sitecore.Commerce.Core]], mscorlib",
   "$values":[
      {
         "$type":"Sitecore.Commerce.Core.LocalizationEntity, Sitecore.Commerce.Core",
         "HasProperties":true,
         "HasComponentsProperties":false,
         "Properties":{
            "$type":"System.Collections.Concurrent.ConcurrentDictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Parameter, Sitecore.Commerce.Core]], mscorlib]], mscorlib",
            "Description":{
               "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Parameter, Sitecore.Commerce.Core]], mscorlib",
               "$values":[
                  {
                     "$type":"Sitecore.Commerce.Core.Parameter, Sitecore.Commerce.Core",
                     "Key":"en",
                     "Value":"This inventory catalog contains inventory for the My Shop Catalog"
                  }
               ]
            },
            "DisplayName":{
               "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Parameter, Sitecore.Commerce.Core]], mscorlib",
               "$values":[
                  {
                     "$type":"Sitecore.Commerce.Core.Parameter, Sitecore.Commerce.Core",
                     "Key":"en",
                     "Value":"My Shop Inventory"
                  }
               ]
            }
         },
         "ComponentsProperties":{
            "$type":"System.Collections.Concurrent.ConcurrentDictionary`2[[System.String, mscorlib],[System.Collections.Concurrent.ConcurrentDictionary`2[[System.String, mscorlib],[System.Collections.Concurrent.ConcurrentDictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Parameter, Sitecore.Commerce.Core]], mscorlib]], mscorlib]], mscorlib]], mscorlib"
         },
         "Components":{
            "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Component, Sitecore.Commerce.Core]], mscorlib",
            "$values":[

            ]
         },
         "DateCreated":"2018-01-11T18:43:34.5333227+00:00",
         "DateUpdated":"2018-01-11T18:43:34.5343245+00:00",
         "DisplayName":"",
         "Id":"Entity-LocalizationEntity-6df7b4690b944b1cb68c4364a3963e86",
         "Version":1,
         "IsPersisted":true,
         "Name":"",
         "Policies":{
            "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Policy, Sitecore.Commerce.Core]], mscorlib",
            "$values":[

            ]
         }
      }
   ]
}

Defining the environment

The final piece of the puzzle is another JSON configuration file where the environment/plugin is defined. These files are placed it src\Sitecore.Commerce.Engine\wwwroot\data\Environments, and in my case I duplicated the PlugIn.Habitat.CommerceShops-1.0.0.json example. It's too big to post here, but the areas I changed were:
  • ArtifactStoreId - to a new GUID
  • Id - to a new ID (the efe49ff743d34b649fffcof293615cd9one)
  • Id - to a new name (the Entity-CommerceEnvironment-HabitatShops one)
  • Name - a new environment name
  • InitialArtifactSets - as follows
InitialArtifactSets for environment

Initialize the environment

Now I just publish the Sitecore.Commerce.Engine project to the four commerce sites (ensuring thumbprints for certificates are setup correctly).

Inside postman, create a new environment to match the one you just created:

New postman environment
The run the following service calls from Postman:
  1. GetToken - under authentication
  2. Bootstrap Sitecore Commerce - under SitecoreCommerce_DevOps
  3. Initialize Environment - under SitecoreCommerce_DevOps
If any of these calls failed (due to 500 error), you will need to take a look in the Commerce Ops log files. If all went smoothly then your new environment is available in the Commerce Business Tools interface!

new environment in Experience Commerce
For me, the only error I got was around some dodgy JSON in the environment/plugin configuration. I had changed the currency elements to NZD and had missed a single USD instance. 

Tuesday, February 20, 2018

Sitecore Experience Commerce - antiforgery error in API calls

When attempting to run some of the Experience Commerce APIs via Postman, several of the import and add methods would fail with a 500 error. Inside the logs of the commerce authoring site, the following was present:
66 09:46:58 INFO Request starting HTTP/1.1 PUT http://localhost:5000/api/ExportInventorySets application/json 95
66 09:46:58 INFO Successfully validated the token.
66 09:46:58 INFO AuthenticationScheme: "BearerIdentityServerAuthenticationJwt" was successfully authenticated.
66 09:46:58 INFO AuthenticationScheme: "Bearer" was successfully authenticated.
66 09:46:58 INFO Authorization was successful for user: "sitecore\Admin".
66 09:46:58 INFO Executed action "Sitecore.Commerce.Plugin.Inventory.ApiController.ExportInventorySets (Sitecore.Commerce.Plugin.Inventory)" in 0.6932ms
66 09:46:58 ERROR Connection id ""0HLBNQFHRI3GP"", Request id ""0HLBNQFHRI3GP:00000001"": An unhandled exception was thrown by the application.
Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The required antiforgery header value "X-XSRF-TOKEN" is not present.
Thanks to the help of Naveed Ahmad in the community slack channel, by editing the AntiForgeryEnabled setting to false in the config.json for the authoring site I was able to call the APIs successfully.

Monday, February 19, 2018

Sitecore Experience Commerce - Extending an inventory item with a plugin

Out of the box, inventory items (otherwise known as a SellableItem) in Sitecore's Experience Commerce provide fields which will cover most eCommerce business requirements. However when a requirement comes up to add additional custom fields, you will need to create a Plugin.

The developer's guide for Experience Commerce 9.0 has a section on creating your first plugin, however sometimes it's easier to understand when given a working example.

Experience Commerce - creating a plugin
Sitecore provides a guide for How to extend Catalog system entities schema in Sitecore Experience Commerce 9.0. I will expand on this to show how to get this example working in your Experience Commerce instance.
  1. Download the Plugin.Sample.Notes.zip example project, which is available on the page above. This is the plugin which adds custom fields to inventory items.
  2. Rename this plugin to fit in with your organisation's naming conventions and modify to the code to meet your requirements. Effectively you replace all instances of Note or Notes and modify the two fields the example adds to meet your requirements (in my case I needed 4 fields).
  3. Extract the commerce SDK to a suitable location (at the time of writing, the version is: Sitecore.Commerce.Engine.SDK.2.0.1922). 
  4. Install the Sitecore.Commerce.Plugin.vsix Visual Studio plugin contained in this folder.
  5. Inside this folder is the Customer.Sample.Solution solution. This contains the commerce engine and a number of sample plugins.
  6. Move your customized Plugin.Sample.Notes project into the src folder (Sitecore.Commerce.Engine.SDK.2.0.1922\src) and then add this project to the solution.
  7. Plugin referenced in the commerce engine
  8. Add this project as a reference to the Sitecore.Commerce.Engine project.
  9. In the Sitecore.Commerce.Engine project, update the config.json values to match that of your commerce sites (authoring, minions, ops and shops). The key values to update are that of: Thumbprint and EnvironmentName.
  10. Ensure the solution builds correctly, you may need the Sitecore Commerce Nugget package source configured: https://sitecore.myget.org/F/sc-commerce-packages/api/v3/index.json
  11. Publish the Sitecore.Commerce.Engine project to your 4 commerce sites (authoring, minions, ops and shops). Of course you should back these up before publishing.
  12. Load up the content editor of your Sitecore instance and click Update Data Templates on the commerce ribbon. 
  13. Sitecore Experience Commerce - Update Data Templates
  14. Now if you open up the inventory manager in the Commerce UI, you are able to view and edit your new custom fields.
Experience Commerce - Extended catalog item
From my experience, any errors during this process often relate to the SLL certificates and their thumbprints.

Sitecore Experience Commerce - Update Data Templates fails

After deploying a custom plugin to extend SellableItem and add my own custom fields, the Update Data Templates method inside the content editor would fail. The Sitecore instance logs showed (key lines shown, some have been removed):
1828 14:12:13 ERROR Authentication Error
Exception: System.Exception
Message: The certificate thumbprint is invalid or missing from your configuration, secure communication with the Commerce Engine is not possible.
1828 14:12:13 ERROR GetList(id='Catalogs',type='Sitecore.Commerce.Plugin.Catalog.Catalog, Sitecore.Commerce.Plugin.Catalog',skip=0,take=1)?$expand=Items($select=Id)
System.Exception: The certificate thumbprint is invalid or missing from your configuration, secure communication with the Commerce Engine is not possible.
I took a look inside the commerce authoring logs (in my case: C:\inetpub\wwwroot\CommerceAuthoring_Sc9\wwwroot\logs) to see if there were any errors in there. The following was present:
18 14:11:06 ERROR ClientCertificateValidationMiddleware: Certificate with thumbprint 803320317DD6250341065603E375906369603AD3 does not have a matching Thumbprint.
Inside the config.json file for this site there was a thumbprint configured which did not match that of the log file above. It turns our that the certificate thumbprint which was configured was that of a different domain (http://MySite instead of http://localhost), changing this certificate configured to that of the log file above (the correct domain matching) allowed the Update Data Templates to complete successfully. 

A handy PowerShell command for tracing a thumbprint to a certificate is as follows:
cd CERT:\\
PS Cert:\> dir -recurse | where {$_.Thumbprint -eq "D4FC5CC9E022C0CFFF24186BE75DA9EC66DAD1C2"} | Format-List -property *
This helped me figure out an incorrect certificate was configured for my four commerce service sites.

Thursday, February 15, 2018

Sitecore 8.2 submit a WFFM form programatically

WFFM in Sitecore is great for allowing content authors to create and manage forms with a wide array of fields and save actions. It's of course possible to extend the module in many ways (custom validators, fields, save actions and so on). But one area where it lacks is on the user experience on the front end. Hence comes the requirement to submit a form programatically, which not only uses the save actions and fields the author created but also hooks into the reporting.

This simple example below, is a condensed example which shows the submission of a form with a single field called Email. Essentially we get the actual form item to have access to it's fields and save actions. This is then used to create the parameters required to submit the form.
var myEmail = "ryan.bailey@email.com";
Sitecore.Forms.Core.Data.FormItem formItem = Sitecore.Forms.Core.Data.FormItem.GetForm(ID.Parse("{7D47FECC-25DF-4561-A911-683F4F164FA7}")); // Get the form item
var emailField = formItem.Fields.Where(x => x.DisplayName == "Email").FirstOrDefault(); // Get a given field - should null check of course

var controlResults = new List<ControlResult>();
controlResults.Add(new ControlResult(emailField.ID.ToString(), emailField.Name, myEmail ,emailField.Parameters,false));

// Parse save actions
var saveActionXml = formItem.SaveActions;
var actionList = Sitecore.Form.Core.ContentEditor.Data.ListDefinition.Parse(saveActionXml);
var actionDefinitions = new List<ActionDefinition>();
actionDefinitions.AddRange(actionList.Groups.SelectMany(x => x.ListItems).Select(li => new ActionDefinition(li.ItemID, li.Parameters) { UniqueKey = li.Unicid }));

Sitecore.Forms.Core.Handlers.FormDataHandler handler = Sitecore.WFFM.Abstractions.Dependencies.DependenciesManager.Resolve<Sitecore.Forms.Core.Handlers.FormDataHandler>();
handler.ProcessForm(ID.Parse("{7D47FECC-25DF-4561-A911-683F4F164FA7}"), controlResults.ToArray(), actionDefinitions.ToArray()); // Submit form
There are older examples available online, but this one works for me on 8.2 and the reporting is there as well.

Sitecore WFFM programmatic submission does not have data in form reports

I had an instance where I needed to submit a WFFM form (Sitecore 8.2) programatically (as we had user experience requirements that needed it sent via a service call). The form was successfully submitting and in the detailed reports showed this fact:

WFFM detailed reports
However the summary report was empty (as in no graph showing the fields where data was submitted):

WFFM summary report empty
The data export was also showing missing data:

WFFM empty form export
After downloading the export in XML instead of a CSV file, I notice that the field nodes in the XML were showing empty IDs (GUIDs), which meant the fields had not been mapped correctly:

WFFM XML export - empty field IDs
The error was due to the way that I was building up the fields list in the programmatic submission. This was an array of ControlResult objects which represented the form fields. I was originally instantiating the object using the following overload:
public ControlResult(string fieldName, object value, string parameters);
However once I started using the overload that took in field ID, everything started working as expected:
public ControlResult(string fieldId, string fieldName, object value, string parameters, bool secure = false);
Please also see my full example of programmatic submission of WFFM forms in Sitecore 8.2.

Sitecore Experience Commerce - Postman could not get any response

When testing out the Postman examples for the Sitecore Experience Commerce APIs, none of them would successfully run and I was getting the following error:
Could not get any response
Postman could not get a response
Opening up the Postman Console (View > Show Postman Console) revealed the following:
Error: SSL Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE
Postman unable to verify leaf signature
To fix this error and allow Postman calls to the Sitecore Experience Commerce APIs to work as expected is as simple as turning off SSL certificate verification (because on a local environment these certificates are self signed).

Postman SSL certificate verification setting

Sitecore Experience Commerce - Customer.Sample.Solution won't build

When starting down the road of creating my own plugin for Experience Commerce, I opened up the Customer.Sample.Solution from version 2.0.1922 of the commerce engine SDK. However the solution (and Sitecore.Commerce.Engine project in particular) would not build due to a missing DLL reference.

Sitecore Commerce Engine missing reference
The Sitecore.Commerce.Plugin.Content reference could not be located. After some digging it appeared that the nuget feed (included with the solution) for Sitecore Commerce was an old preview feed which was no longer active.

Invalid Sitecore Experience Commerce nuget feed
Updating this to the live Sitecore Commerce nuget feed allowed the reference to be found and the solution to build.

Wednesday, February 14, 2018

Sitecore 8.2 WFFM some data not appearing in form reports

A strange issue came up whereby the data inside the form report for a WFFM form in Sitecore 8.2 was not all appearing. In this case the form consisted of three fields: Email address, first name and last name. There was a long history of all of the data appearing in the form report, then suddenly email address stopped recording.

Data disappearing from WFFM form reports
After some investigation it turned out that the Save checkbox on the Email address field (under the form node) had somehow been unchecked. Re-checking this field, publishing the form and submitting test data confirmed it to fix the issue of some data being missing in form reports.

Save option on a WFFM form field

TDS code generation in Sitecore with Glass Mapper

Team Development for Sitecore is a great tool for reducing deployment time by making it nice and easy to move Sitecore items between environments. Likewise Glass Mapper provides ORM functionality from those same Sitecore items into your code (it gives you models of Sitecore item and provides decouples the Sitecore API for item access).

A great feature available with TDS is code generation. What this means is that any template that you serialize into a TDS template can automatically generate a model ready for use. This saves a lot of development time along with minimizing the chance for error. Gone are the days where a slight misspelling on the hand written model would cause problems, as would someone changing the template and forgetting to update the model.

Setting up code generation for TDS

The first step is to download the Text Template Transformation Toolkit (T4) templates for TDS. These are used to generate the C# models. The download is available on Github, and it's the files ending in .tt that we are after. I will often add these to a folder in the solution where they can be referenced by all TDS projects.

T4 templates for TDS

In the project where the output of code generation will go we need the following:
  • In the root of the project you need a class where the models will be output, I generally call mine Master.cs.
  • A reference to the Glass.Mapper.Sc nuget package.
  • A reference to the Sitecore.Kernel.dll for the version of Sitecore your instance is developed on. I use the Sitecore public nuget feed for this.
In the TDS project the following needs to be setup:
  • On the code generation tab, check the box to Enable Code Generation.
  • This will cause a Code Generation Templates item to appear under the TDS project in visual studio. On this node right add the T4 templates (add existing items).
  •  
    A tip for this is to select Add as link, which allows all TDS projects to share the same T4 templates rather than creating a copy per project.
  • On the code generation tab for the TDS project:
    • Select the target project where the code will be output
    • Select the file which will contain the models (this needs to be in the root of the project).
    • Select the base namespace for the file.
    • Reference the header transform file - GlassV3Header.tt
    • Reference the base project transform file - GlassV3Item.tt
  • Check the target file (master.cs), the models should have auto-generated.
It is also possible to have all code regenerate for a TDS project by right clicking it and selecting Re-Generate Code for All Items.

My solution for this example, looked as follows:

Example solution for TDS code generation

Tuesday, February 13, 2018

How catalogs, pricing and promotions are structured in Experience Commerce 9

When picking up Sitecore's Experience Commerce 9 for the first time, one area which can be tricky to conceptualize is the structure of pricing and promotions. This post aims to explain this structure and provide base knowledge for anyone new to Experience Commerce. To start off with, is my very rough diagram of pricing and promotions:

Experience Commerce 9 pricing structure

At a high level, the blue (larger) boxes are the key areas each of which have their own administration section. The smaller green boxes are children of the section above (for example a price book will contain price cards). It's also worth noting that, as the diagram flows down, there is a 1 to many relationship (this means that an inventory set can contain may catalogs, a catalog can have many price books and so on).

Experience Commerce Business Tools

Pricing and promotions for Sitecore Experience Commerce is administered on a separate web site from the main Sitecore administration area. This can be access via the Commerce Business Tools shortcut on the Sitecore dashboard:

Business Tools shortcut on the Sitecore dashboard
Which will lead to the Experience Commerce dashboard:

Experience Commerce Dashboard
This web site contains a lot of functionality, however for this post we will be focusing on the Merchandising, Inventory, Pricing and Promotions sections.

Inventory set

Inventory sets appear under the Inventory section inside the Experience Commerce Business Tools administration web site. An inventory set will contain the products ranged by a particular store. These products are all stored in one large bucket and are not categorized at this level.

A product is able to be transferred to another inventory set as well. 

Sitecore Experience Commerce Inventory Set

Catalog

Catalogs will appear under the Merchandising section inside the Experience Commerce Business Tools administration web site. A catalog will reference an inventory set and allows you to create categories which can then contain products. These products are either referenced from the same inventory set or created directly in the catalog (these ones will not be contained in the inventory set).

Sitecore Experience Commerce Catalog

Price Book

Price books appear under the Pricing section inside the Experience Commerce Business Tools administration web site. A price book is able to be associated with multiple catalogs and will contain many price cards. 

Products contain their list price (which is the RRP), these price books are more to control multi-buy pricing. What this means is that when the user buys x of a product the price is y. It allows discounting of price based on the number selected.

Experience Commerce Price Books

Price Cards and Snapshots
A price card contains a number of Snapshots which is date/time scheduling for the pricing (run through workflow as well). This is a level on top of the pricing which will allow merchandisers to setup pricing in advance for any campaigns, promotions, etc. Once you select a given snapshot, you get access to set the actual pricing.

This is done once for each currency, and then you are able to configure as many prices as required. The example below has two pricing examples for USD: Buy 1 for $600 and Buy 2 for $500 (each).

Experience Commerce Price Snapshot
A price card is assigned to a given product, so you would likely have one of these setup for each product (or group of products) which require volume based discounting. 

Promotion Book

Promotion Books appear under the Promotions section inside the Experience Commerce Business Tools administration web site. A promotion book much like a price book is able to be associated with multiple catalogs. Contained within the promotion book are promotion items.

Experience Commerce Promotions

Promotions
Each promotion has Qualifications which is rules based for when the promotion will apply (for example the cart value is above a certain amount). They also have Benefits which is rules based outcomes for the promotion (such as discount the cart total by 10%).

Promotions can automatically apply themselves once the qualification is met, or a coupon may need to be entered. If a user enters the coupon for a given promotion and they meet the qualification, then the benefit will be added.

Promotions in Experience Commerce is a huge area with countless different configurations that lead to many outcomes. For the purposes of this post I have kept the details light, however be aware there is a lot of power in this section.

Pricing in action

With the Storefront example site which comes with Sitecore Experience Commerce 9, the pricing will work as follows:
  • A inventory set contains all the products
  • A catalog contains a number of categories which reference the products in the inventory set. This is what is displayed to the front-end.
  • There is a price book which contains some volume based price cards. On the product detail page the list price will always display, however it's inside the cart where the price card pricing is shown. Custom business logic would be needed to display price book information on the product detail page for examples.
  • The promotions in the promotion book are applied as the various qualifications are met.

Monday, February 12, 2018

Sitecore Experience Commerce - Commerce Business Tools won't load

When attempting the load the Commerce Business Tools in Sitecore Experience Commerce 9.0, the page would stay in a loading state indefinitely.

Experience Commerce Business Tools doesn't load
Looking in the console were several errors related to the GetNavigationView API call:
Http failure response for https://localhost:5000/api/GetNavigationView()
This traces back to the Commerce Authoring API (based on the port number and the IIS websites on my local machine). Looking into the logs for this (located at C:\inetpub\wwwroot\CommerceAuthoring_Sc9\wwwroot\logs for me) dug out the following error:
ERROR CtxMsg.Error.InvalidShopCurrency: Text=Currency 'USD' for Shop 'CommerceEngineDefaultStorefront' was not found.
This could also be seen in the console inside chrome under Network and XHR:

Currency error in the console
This error seems to occur with the default selection of HabitatAuthoring inside the Commerce Business Tools interface. After completing an IIS reset and reloading the page, the error appeared to have disappeared.

Experience Commerce Business Tools working
However this might take a couple tries to "correct itself"...

Monday, February 5, 2018

Sitecore multiple types were found that match the controller

After deploying a set of MVC controllers to my Sitecore 9 instance (with Experience Commerce 9.0), the following error appeared after attempting to call the controller via JavaScript.
Multiple types were found that match the controller named 'Account'. This can happen if the route that services this request ('CommonApi/{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter. 
The request for 'Account' has found the following matching controllers:
Sitecore.Commerce.XA.Feature.Account.Controllers.AccountController
MyProject.Foundation.API.Areas.CommonApi.Controllers.AccountController
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 
Multiple types were found that match the controller named 'Account'.
This was occurring because both my custom code and the Commerce Experience Accelerator had an account controller - even though my controller was contained inside an MVC area. The fix for this, was to reference the namespace when I defined the route.
RouteTable.Routes.MapRoute("Login","CommonApi/{controller}/{action}/{id}", new { area = "CommonApi", controller = "Account", action = "Login", id = UrlParameter.Optional }, new[] { "MyProject.Foundation.API.Areas.CommonApi.Controllers" });

Visual Studio project in solution build fails with no error

I have a Visual Studio solution containing multiple projects all using version 4.7 of the .NET framework. When attempting to build the solution, a single project would fail, yet provide no error message.

Single project fails build
I was able to figure out that the project only failed build when it referenced another project in the solution. The build order inside the solution appeared correct (right click solution and select project build order).

This problem was occurring in Visual Studio 2015, however after opening it in Visual Studio 2017 the projects would all build as expected.

Using MVC Areas with your Sitecore Helix solution

One of the great features of a Sitecore solution which uses the Helix approach is the modular architecture of all of your features. This allows for logical grouping of features by type or perhaps website (in a multi-site implementation).

However as a solution expands, it may become possible for there to be conflicts on controller names. Therefore the use of MVC areas is one way to partition the controllers before any conflicts happen. In my case I was building a rather large helix solution for a multi-site commerce implementation, where there was likely to be controllers with duplicate names. In this case I was creating a feature project for each website (along with a common) to reduce build time (as opposed to logically grouping features, such as an Account feature project for all sites).

This is where areas came in handy, inside the project it was simple enough to add one.

Visual Studio -  Add MVC Area
Inside Sitecore it's as simple as defining the area on the controller rendering.

Sitecore controller rendering - define the area
In the example above, there is the Testing area which contains the Common Feature Controller with a Test view.

Of course using MVC areas is not always relevant for every Sitecore implementation, however it is a good tool to have handy when required.

Sunday, February 4, 2018

Unit testing, Mocks and Dependency Injection in Sitecore 9

In this post I am going to cover how to inject your own dependencies into Sitecore 9's default implementation (Microsoft.Extensions.DependencyInjection abstractions from ASP.NET Core). On top of this I am going to show how these dame dependencies can be unit tested with Mocks.

The technologies which will be used are:
  • NUnit - Unit testing framework
    • In addition the NUnit 3 Test Adapter for Visual Studio
  • Moq - Mocking framework for .NET
  • Sitecore 9 (update-1)

Setting up the logic to inject

The first step is to create an interface and implementer which will be what is injected into Sitecore. In a real world example this may be an external service layer or perhaps a data access layer. In this case it's a simple example of an account interface.

First up is the model which is the return type for the method.
namespace MyProject.Foundation.ServiceLayer.Account.Model
{
    public class UserModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string EmailAddress { get; set; }
    }
}
This is the implementation of the IAccountService interface.
namespace MyProject.Foundation.ServiceLayer.Account
{
    public class AccountService : IAccountService
    {
        public UserModel GetUser(string emailAddress)
        {
            // logic here

            return new UserModel
            {
                EmailAddress = "Ryan.Bailey@email.com",
                FirstName = "Ryan",
                LastName = "Bailey"
            };
        }
    }
}
This is the IAccountService interface.
namespace MyProject.Foundation.ServiceLayer.Account
{
    public interface IAccountService
    {
        UserModel GetUser(string emailAddress);
    }
}
It's a simple example, but normally the GetUser implementation would do a lot more (such as call a service or read from a database). I also prefer to use models rather than parameters direct (such as email address in this case).

Unit testing with mocks

Now we have an interface (and implementer) with a single method, it's a good idea to create a unit test. Of course the unit test shouldn't actually be calling a service or reading from database, so we need to use the concept of Mocks. Mocks allow us to test and focus on the code being tested, rather than worrying about external dependencies (which integration testing should cover).

A new Unit Test Project should be created in your Visual Studio solution, the following is an example of a test for the GetUser method. 
using System;
using NUnit.Framework;
using MyProject.Foundation.ServiceLayer.Account;
using Moq;
using MyProject.Foundation.ServiceLayer.Account.Model;

namespace MyProject.Foundation.ServiceLayer.Tests.Account
{
    [TestFixture]
    public class AccountUnitTest
    {
        private Mock<IAccountService> mockAccountService;

        [OneTimeSetUp]
        public void SetupTests()
        {
            mockAccountService = new Mock<IAccountService>();

            // GetUser
            var user = new UserModel
            {
                EmailAddress = "Ryan.Bailey@email.com",
                FirstName = "Ryan",
                LastName = "Bailey"
            };

            mockAccountService.Setup(m => m.GetUser(It.IsAny<string>())).Returns(user);
        }

        [Test]
        public void TestGetUser()
        {
            var user = mockAccountService.Object.GetUser("test@email.com");

            Assert.AreEqual("Ryan", user.FirstName);
            Assert.AreEqual("Bailey", user.LastName);
        }
    }
}
In this example our TestFixture makes use of the OneTimeSetUp attribute to perform setup actions before running through the Tests. In here we Mock the IAccountService interface and it's GetUser method. In this example, any request for GetUser (note the IsAny on the email address string parameter), will return the mocked data.

Further down in the actual test, the GetUser method is called on the mock service and we are asserting that the data matches what was expected - this unit test will pass.

Test Explorer in Visual Studio

This again is a basic example, for a unit test on a login method, you may mock out a successful and failed login and then have a unit test for each. The idea is to test all aspects of the model/code to ensure complete unit test coverage.

Hooking into Sitecore 9 Dependency Injection

Now that we have our IAccountService interface, it would be nice to inject this and access it in our MVC controllers. We are going to register our dependency in code by implementing Sitecore's IServicesConfigurator.
using MyProject.Foundation.ServiceLayer.Account;
using Sitecore.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

namespace MyProject.Foundation.BackEnd.Common.DependencyInjection
{
    public class MyServicesConfigurator : IServicesConfigurator
    {
        public void Configure(IServiceCollection serviceCollection)
        {
            serviceCollection.AddScoped(typeof(IAccountService), typeof(AccountService));

            serviceCollection.AddMvcControllers("MyProject.Feature.*");
        }
    }
}
The AddScoped call comes with Sitecore and allows us to register our dependency quite simply. The AddMvcControllers call is a custom configurator from Kam Figy, which allows us to easily register the MVC Controllers we wish to dependency inject into. As I am following Sitecore Helix I can register all of my features in one simple line.

The custom configurator is below:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace MyProject.Foundation.BackEnd.Common.DependencyInjection
{
    public static class ServiceCollectionExtensions
    {
        [MethodImplAttribute(MethodImplOptions.NoInlining)]
        public static void AddMvcControllersInCurrentAssembly(this IServiceCollection serviceCollection)
        {
            AddMvcControllers(serviceCollection, Assembly.GetCallingAssembly());
        }

        public static void AddMvcControllers(this IServiceCollection serviceCollection, params string[] assemblyFilters)
        {
            var assemblyNames = new HashSet<string>(assemblyFilters.Where(filter => !filter.Contains('*')));
            var wildcardNames = assemblyFilters.Where(filter => filter.Contains('*')).ToArray();

            var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(assembly =>
            {
                var nameToMatch = assembly.GetName().Name;
                if (assemblyNames.Contains(nameToMatch)) return true;

                return wildcardNames.Any(wildcard => IsWildcardMatch(nameToMatch, wildcard));
            })
            .ToArray();

            AddMvcControllers(serviceCollection, assemblies);
        }

        public static void AddMvcControllers(this IServiceCollection serviceCollection, params Assembly[] assemblies)
        {
            var controllers = GetTypesImplementing<IController>(assemblies)
                .Where(controller => controller.Name.EndsWith("Controller", StringComparison.Ordinal));

            foreach (var controller in controllers)
            {
                serviceCollection.AddTransient(controller);
            }

            // h/t Sean Holmesby and Akshay Sura: this adds Web API controller support
            //var apiControllers = GetTypesImplementing<ApiController>(assemblies)
               // .Where(controller => controller.Name.EndsWith("Controller", StringComparison.Ordinal));

            //foreach (var apiController in apiControllers)
            //{
                //serviceCollection.AddTransient(apiController);
            //}
        }

        public static Type[] GetTypesImplementing<T>(params Assembly[] assemblies)
        {
            if (assemblies == null || assemblies.Length == 0)
            {
                return new Type[0];
            }

            var targetType = typeof(T);

            return assemblies
                .Where(assembly => !assembly.IsDynamic)
                .SelectMany(GetExportedTypes)
                .Where(type => !type.IsAbstract && !type.IsGenericTypeDefinition && targetType.IsAssignableFrom(type))
                .ToArray();
        }

        private static IEnumerable<Type> GetExportedTypes(Assembly assembly)
        {
            try
            {
                return assembly.GetExportedTypes();
            }
            catch (NotSupportedException)
            {
                // A type load exception would typically happen on an Anonymously Hosted DynamicMethods
                // Assembly and it would be safe to skip this exception.
                return Type.EmptyTypes;
            }
            catch (ReflectionTypeLoadException ex)
            {
                // Return the types that could be loaded. Types can contain null values.
                return ex.Types.Where(type => type != null);
            }
            catch (Exception ex)
            {
                // Throw a more descriptive message containing the name of the assembly.
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
                    "Unable to load types from assembly {0}. {1}", assembly.FullName, ex.Message), ex);
            }
        }

        /// <summary>
        /// Checks if a string matches a wildcard argument (using regex)
        /// </summary>
        private static bool IsWildcardMatch(string input, string wildcards)
        {
            return Regex.IsMatch(input, "^" + Regex.Escape(wildcards).Replace("\\*", ".*").Replace("\\?", ".") + "$", RegexOptions.IgnoreCase);
        }
    }
}
Of course we will need to register the MyServicesConfigurator code with Sitecore (via configuration) to ensure it is injected correctly.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
    <sitecore>
        <services>
            <configurator type= "MyProject.Foundation.BackEnd.Common.DependencyInjection.MyServicesConfigurator, MyProject.Foundation.BackEnd.Common"/>
        </services>
    </sitecore>
</configuration>

Calling the injected interface

Now that we have injected our interface into the Sitecore DI provider, it's time to access it via an MVC controller. This is quite simply done as illustrated below:
using System.Web.Mvc;
using MyProject.Feature.Common.Models;
using MyProject.Foundation.ServiceLayer.Account;

namespace MyProject.Feature.Common.Controllers
{
    public class CommonFeatureController : Controller
    {
        private readonly IAccountService _accountService;

        public CommonFeatureController(IAccountService accountService)
        {
            _accountService = accountService;
        }

        public ActionResult Test()
        {
            var user = _accountService.GetUser("test@email.com");

            var model = new TestModel
            {
                EmailAddress = user.EmailAddress
            };

            return this.View(model);
        }
    }
}
In this example, I have a CommonFeatureController with a Test method which returns a model to the Test view. It's simply trying to show how the AccountService can be accessed and called and data then passed through to the view.

Friday, February 2, 2018

Sitecore Experience Commerce - Empty strings are not allowed after clean install

After installing Experience Commerce 9.0 successfully and attempting to load the front-end website I was greeted with the following error message:
Empty strings are not allowed.
Parameter name: value
With the stack trace:
[ArgumentException: Empty strings are not allowed.
Parameter name: value]
   Sitecore.Diagnostics.Assert.ArgumentNotNullOrEmpty(String argument, String argumentName) +204
   Sitecore.Data.ID.Parse(String value) +30
   Sitecore.Commerce.XA.Foundation.Common.Models.CommerceStorefront.get_ControlPanel() +105
   Sitecore.Commerce.XA.Foundation.Common.Models.CommerceStorefront.get_StorefrontConfiguration() +10
   Sitecore.Commerce.XA.Foundation.Common.Pipelines.SecuredPageProcessor.IsSslEnforcedForStorefront() +35
   Sitecore.Commerce.XA.Foundation.Common.Pipelines.SecuredPageProcessor.Process(PipelineArgs args) +164
   (Object , Object[] ) +74
   Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args) +469
   Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain) +22
   Sitecore.Web.RequestEventsHandler.OnPostAuthenticateRequest(HttpContextBase context) +214
   Sitecore.Nexus.Web.HttpModule.’ (Object “ , EventArgs ” ) +335
   System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +136
   System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step) +195
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +88
Sitecore commerce - empty strings are not allowed
This error message appears because on a default instance the example Storefront site (with content) is not installed and instead you get an empty site. In fact the home node by default actually has no presentation details.

Experience Commerce empty site
What you need to do is inser a new Site under the Sitecore node:

Sitecore Experience Commerce - new site
Ensuring that the following 3 features are selected to get the full Storefront demo:

Sitecore Experience Commerce - new site features
As well as selecting the correct theme:

Sitecore Experience Commerce - new site theme
In my case, I then deleted the empty Storefront site and renamed this new one Storefront. Performed a full publish and the front-end loaded correctly.

Sitecore Experience Commerce - Storefront demo site
In cases where you wish to build out a site from scratch, this demo site is still good to use as a reference to assist in building the required pages/components.