Thursday, February 22, 2018

Sitecore Experience Commerce - Adding a new environment initialized with data

For a detailed explanation of Commerce roles and how they tie in with environments, please see my dedicated post. This gives an overview of environments and I recommend reading before continuing with this post.

This post is showing how to define an environment (that comes pre-loaded with data, much like the Habitat example that comes with Experience Commerce) and add it into the commerce business tools. To simply add an empty environment, please see: Adding a new empty environment.

In this case the data defined is a simple inventory set, however Habitat takes this 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 - this would be the same for a given set of environments (authoring, shops and minions)
  • 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

Add the environment to global

Open up the Global.json file that comes inside the bootstrap folder of the Sitecore.Commerce.Engine project. There will be a GlobalEnvironment section which contains the values of the environments to appear in business tools. Here you can add the new environment name. 

Sitecore Experience Commerce - Environment for business tools

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. 

No comments:

Post a Comment