Introduction

In almost all eCommerce implementations the final step is to send orders created in the online channel to a back office system such as an ERP system for fulfillment. During the process, it is also necessary to create the relevant customer record in the back office. The following example was built for Dynamics 365 for Operations (Dynamics AX) but can serve as an example on how to send orders and customers to any 3rd party system.

As we all know, there is more to an order integration solution than just sending the order once to an ERP. For instance, the order integration may not be successful the first time for a multitude of reasons and will need to be re-sent either manually or via a timed process. I will cover the modification needed to resend an order in another post.

For now, we will just cover the initial sending of the order and the customer at the same time.

The example approach is made of the following artifacts, further explanation of each artifact is included in each section:-

  1. A new pipeline block extending the functionality of the “ReleasedOrdersMinion”.
  2. A new policy to store the connectivity details to your ERP (Dynamics 365) systems API. For instance, the API URL and credentials.
  3. Two new components to store custom fields on the Customer and Order entities to track the integration status.
  4. An integration helper class that contains the reusable logic to connect to the ERP and send the orders and customers.

This article assumes that you have downloaded and installed Sitecore Commerce, Postman and have the demo environment and SDK ready to use.

If you know what the components above are and how to create them you can skip the explanations and go straight to the code further down.

What is a minion and in particular, what is the “Released orders minion” and how do we customize it?

Very simply, a minion is an asynchronous task that runs on a periodic basis to handle certain business functions without user interaction. In this example we are trying to send orders to our ERP, so we are going to modify the “Released orders Minion” by extending its functionality using a new pipeline block (Will explain this later).

The “Released orders Minion” runs against a list of orders which are in the “Released” state and changes them to the “Completed” state.

How do we get an order to the “Released” state?

Another minion named the “Pending orders minion” runs against all the orders in the “Pending” state and changes them to the “Released” state.

To clarify the process is as follows in Sitecore Commerce…

  1. The order is created, the state is “Pending”.
  2. The “Pending orders Minion” is run, the state is “Released”
  3. The “Released orders Minion” is run, the state is “Completed”

So, how do we run these minions?

On a schedule

Minions are designed to be run on schedule, the default is every five minutes but this can be changed in your environment file as per this example…

Minion setup

Manually

Your minions might not be setup to run by themselves depending on how your environment is setup, to run them manually follow these steps…

  1. Download and install Postman.
  2. Open Postman and from the file menu, import all the example API calls from this location in the Sitecore Commerce SDK “Postman’ folder. For example “C:\Development\Sitecore.Commerce.SDK.1.1.819\Postman”
  3. On the menu on the left go to the “SitecoreCommerce_DevOps” folder and under “Minions” section you will see two examples to execute “Pending orders Minion” and the “Release orders Minion”.
  4. Double click to open them in the workspace and execute them

Note: You still may need to wait 5 minutes for the jobs to run i.e. they don’t necessarily execute synchronously.

Modifying the Sitecore Commerce engine to send orders to an ERP

We are going to modify the engine and change the orders process to do the following:-

  1. The order is created, the state is “Pending”. (Unchanged)
  2. The “Pending orders Minion” is run, the state is “Released”. (Unchanged)
  3. The “Released orders Minion” is run, the order and customer will be sent to the ERP system and the status will be changed to “Completed – Sent to back office” if successful and “Problem – Not sent to back office” if it fails. The integration status will be stored against the customer and order and persisted to the data store.

Now that we know what minions are, which ones are involved with changing orders status and what our end result should be, let’s get into the code…

Open the SDK example solution and create a new plugin project

  1. Ensure you have the Sitecore Commerce Visual Studio extension installed. For example located here, “C:\Development\Sitecore.Commerce.SDK.1.1.819\SitecorePluginTemplate.vsix”
  2. In the SDK folder open the “Customer.Sample.Solution.sln”. For example, “C:\Development\Sitecore.Commerce.SDK.1.1.819\Customer.Sample.Solution.sln”
  3. Right-click on the solution and add a new project of type “Sitecore Commerce plugin project” located under “web”.
  4. Call the project something like “Sitecore.Commerce.Connectors.MYERP.Plugin”
  5. If you get errors regarding loading packages with a message like this “Unable to resolve ‘Sitecore.Commerce.Core (>= 1.1.0)’ for ‘.NETFramework,Version=v4.6.1’.”. Open the “projects.json” file and change this line “”dependencies”: {
    “Sitecore.Commerce.Core”: “1.1.*” to read “”dependencies”: {
    “Sitecore.Commerce.Core”: “1.1.819”, in other words in must reflect your commerce version.
  6. Delete all the folders except the “Pipelines” and “Policies”
  7. Under “Pipelines” delete all the classes
  8. Under “Policies” delete all the classes.
  9. Open the “projects.json” file.
  10. Under dependencies add the following lines  “Sitecore.Commerce.Plugin.Orders”: “1.1.819”,
    “Sitecore.Commerce.Plugin.Customers”: “1.1.819”. The versions must match your engine version.
  11. Rebuild the solution
  12. Open the “projects.json” file located under the  “Sitecore.Commerce.Engine” project and add a dependency to your new plugin by adding the following line “Sitecore.Commerce.Connectors.MYERP.Plugin”: “0.0.1”, if you are not using this name for your plugin change it.

Create a new policy to hold the connection details to your ERP system

Here we will create a policy that will store our connection details to our ERP system, in this example it is Dynamics 365 for operations (Dynamics AX).  We can then store the values required to make the connection to the ERP in the environments JSON files. Change this policy to store the values required to connect to your specific back office system if necessary.

  1. Under the “Policy” folder add a new class and call it “ErpPolicy”
  2. Copy the following code into the class file, you may need to change namespaces etc.


namespace Sitecore.Commerce.Connectors.MYERP.Plugin.Policies
{
using Sitecore.Commerce.Core;
/// <summary>
/// The policy.
/// </summary>
public class ErpPolicy : Policy
{
/// <summary>
/// Initializes a new instance of the <see cref="ErpPolicy"/> class.
/// </summary>
public ErpPolicy()
{
this.AosUrl = string.Empty;
this.ActiveDirectoryTenant = string.Empty;
this.ApplicationId = string.Empty;
this.ApplicationKey = string.Empty;
this.RetailServerUrl = string.Empty;
}
/// <summary>
/// Gets or sets the application object server url.
/// </summary>
public string AosUrl { get; set; }
/// <summary>
/// Gets or sets the retail server url.
/// </summary>
public string RetailServerUrl { get; set; }
/// <summary>
/// Gets or sets the application id.
/// </summary>
public string ApplicationId { get; set; }
/// <summary>
/// Gets or sets the application key.
/// </summary>
public string ApplicationKey { get; set; }
/// <summary>
/// Gets or sets the active directory tenant.
/// </summary>
public string ActiveDirectoryTenant { get; set; }
}
}

view raw

ErpPolicy.cs

hosted with ❤ by GitHub

Browse to your environments files, add the new policy section and enter your ERP connection details by following these steps…

You may need to complete the following steps for all your environment files depending on your setup.

  1. Go to your environments folder and open your environment file, for example, located here “C:\Development\Sitecore.Commerce.SDK.1.1.819\Sitecore.Commerce.Engine\wwwroot\data\Environments\PlugIn.Habitat.CommerceMinions-1.0.0.json”
  2. Add the following to the policies section. There is a full environments file example located here


{
"$type": "Sitecore.Commerce.Connectors.MYERP.Plugin.Policies.ErpPolicy, Sitecore.Commerce.Connectors.MYERP.Plugin",
"ActiveDirectoryTenant": "https://login.windows.net/MYAD-0b55-4c91-81c1-c0377a0d2cac",
"AosUrl": "https://MYAOS.cloudax.dynamics.com",
"ApplicationId": "MYAPPID-4722-4641-86da-d96b5c5281c3",
"ApplicationKey": "MYKEY8j9nSQNJEeuVjAvZ8AojyQIotaZlMILeo=",
"RetailServerUrl": "https://MYRETAILSERVER.cloudax.dynamics.com"
}

In order for the engine to recognize the new settings in your environments file, you need to re-bootstrap your environments. This process reloads the environment.json files into the data store (SQL Server by default).

Re-boot strap by following these steps:-

  1. Open Postman
  2. Go to the “Sitecore Commerce DevOps” folder
  3. Under “Environment bootstrap” folder run the “Bootstrap Sitecore Commerce” API call.

To verify that the new environment files were loaded

  1. Make a connection to your SQL server using the SQL management studio.
  2. In the database “SitecoreCommerce_Global” query the “CommerceEntities” table using the code below. You should see that your plugin is now referenced by the environment entities. If this query returns nothing your environments were not updated.


SELECT TOP 5 [Id]
,[EnvironmentId]
,[Version]
,[Entity]
FROM [SitecoreCommerce_Global].[dbo].[CommerceEntities]
where entity like '%MYERP%'

Creating a new component to store the integration status of the order and the customer

We will create a set of new components to store the integration status and key fields against the customer and order entities. These new fields are persisted to the data store (SQL Server) automatically for us by the engine.

Add the new component classes

  1. In your “Sitecore.Commerce.Connectors.MYERP.Plugin” project add a folder called “Components”.
  2. In the “Components” folder add a new class called “ErpStatusComponentBase.cs” and copy the following code…


namespace Sitecore.Commerce.Connectors.MYERP.Plugin.Components
{
using System;
using Sitecore.Commerce.Core;
/// <summary>
/// The customer status component base.
/// </summary>
public abstract class ErpStatusComponentBase : Component
{
/// <summary>
/// Gets or sets a value indicating whether integrated.
/// </summary>
public bool Integrated { get; set; }
/// <summary>
/// Gets or sets the status message.
/// </summary>
public string StatusMessage { get; set; }
/// <summary>
/// Gets or sets the application url.
/// </summary>
public string ErpUrl { get; set; }
/// <summary>
/// Gets or sets the date sent.
/// </summary>
public DateTime StatusDate { get; set; }
}
}

  1. In the “Components” folder add a new class called “ErpOrderStatusComponent.cs” and copy the following code…


namespace Sitecore.Commerce.Connectors.MYERP.Plugin.Components
{
/// <summary>
/// The order status component.
/// </summary>
public class ErpOrderStatusComponent : ErpStatusComponentBase
{
/// <summary>
/// Gets or sets the destination order number.
/// </summary>
public string ErpOrderNumber { get; set; }
}
}

  1. In the “Components” folder add a new class called “ErpCustomerStatusComponent.cs” and copy the following code…


namespace Sitecore.Commerce.Connectors.MYERP.Plugin.Components
{
/// <summary>
/// The customer status component.
/// </summary>
public class ErpCustomerStatusComponent : ErpStatusComponentBase
{
/// <summary>
/// Gets or sets the destination order number.
/// </summary>
public string ErpCustomerNo { get; set; }
}
}

Creating an integration helper class that contains the reusable logic to create the order and the customer in the ERP

We now have the plumbing code to store our values for connectivity in a policy and the result of the integration process in the entity components.

We will now create the code to handle the calling of the destination ERP API and the setting of the integration fields once the call to the ERP is complete.

Please note, this example code always returns “true”, you must write the actual implementation code.

Add the integration helper class

  1. In your “Sitecore.Commerce.Connectors.MYERP.Plugin” project add a folder called “Erp”.
  2. In the “Erp” folder add a new class called IntegrationHelper.cs.
  3. Copy the following code over the new class, you may need to change namespaces etc.


namespace Sitecore.Commerce.Connectors.MYERP.Plugin.Erp
{
using System;
using System.Threading.Tasks;
using Sitecore.Commerce.Connectors.MYERP.Plugin.Components;
using Sitecore.Commerce.Connectors.MYERP.Plugin.Policies;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Plugin.Customers;
using Sitecore.Commerce.Plugin.Orders;
/// <summary>
/// The integration helper.
/// </summary>
public class IntegrationHelper
{
/// <summary>
/// The persistEntityPipeline.
/// </summary>
private readonly IPersistEntityPipeline persistEntityPipeline;
/// <summary>
/// The get customer pipeline.
/// </summary>
private readonly IFindEntityPipeline findEntityPipeline;
private readonly IServiceProvider serviceProvider;
private readonly IGetCustomerPipeline getCustomerPipeline;
/// <summary>
/// Initializes a new instance of the <see cref="IntegrationHelper"/> class.
/// </summary>
/// <param name="persistEntityPipeline">
/// The persist entity pipeline.
/// </param>
/// <param name="findEntityPipeline1">
/// The get customer pipeline 1.
/// </param>
/// <param name="serviceProvider"></param>
/// <param name="getCustomerPipeline"></param>
public IntegrationHelper(IPersistEntityPipeline persistEntityPipeline, IFindEntityPipeline findEntityPipeline1, IServiceProvider serviceProvider, IGetCustomerPipeline getCustomerPipeline)
{
this.persistEntityPipeline = persistEntityPipeline;
this.findEntityPipeline = findEntityPipeline1;
this.serviceProvider = serviceProvider;
this.getCustomerPipeline = getCustomerPipeline;
}
/// <summary>
/// The translate order and send.
/// </summary>
/// <param name="order">
/// The order.
/// </param>
/// <param name="context">
/// The context.
/// </param>
/// <returns>
/// The <see cref="Task"/>.
/// </returns>
public async Task<Order> TranslateOrderAndSendToErp(Order order, CommercePipelineExecutionContext context)
{
try
{
var customer = await this.UpdateCreateCustomer(order, context);
var persistOrderArgument = await this.UpdateCreateOrder(order, context, customer);
return persistOrderArgument.Entity as Order;
}
catch (Exception e)
{
throw e;
}
}
private async Task<PersistEntityArgument> UpdateCreateOrder(
Order order,
CommercePipelineExecutionContext context,
Customer customer)
{
// Get the policy for the ERP connection
var erpPolicy = context.GetPolicy<ErpPolicy>();
// Create order >>
var orderId = order.OrderConfirmationId;
//!!!!!REPLACE THIS WITH YOUR CALL TO THE ERP use erpPolicy to get connection details!!!!
var orderIntegrated = true;
order.GetComponent<ErpOrderStatusComponent>().StatusDate = DateTime.Now;
order.GetComponent<ErpOrderStatusComponent>().Integrated = orderIntegrated;
// Update the order status on success
if (orderIntegrated)
{
order.Status = "Completed – Sent to ERP";
order.GetComponent<ErpOrderStatusComponent>().ErpUrl = erpPolicy.AosUrl;
order.GetComponent<ErpOrderStatusComponent>().StatusMessage = "Success";
if (customer == null)
{
order.GetComponent<ErpOrderStatusComponent>().StatusMessage =
order.GetComponent<ErpOrderStatusComponent>().StatusMessage + " – anonymous customer";
}
//!!!!!REPLACE THIS WITH YOUR ERPs ORDER NUMBER!!!!!!
order.GetComponent<ErpOrderStatusComponent>().ErpOrderNumber = "Ord-" + order.OrderConfirmationId;
}
else
{
order.Status = "Problem – Not sent to ERP";
order.GetComponent<ErpOrderStatusComponent>().ErpUrl = erpPolicy.AosUrl;
order.GetComponent<ErpOrderStatusComponent>().StatusMessage = "There was an error while sending the data to ERP";
}
// Update the order
var persistOrderArgument = await this.persistEntityPipeline.Run(new PersistEntityArgument(order), context);
return persistOrderArgument;
}
private async Task<Customer> UpdateCreateCustomer(Order order, CommercePipelineExecutionContext context)
{
// Get the policy for the D365 connection
var erpPolicy = context.GetPolicy<ErpPolicy>();
// *** Create customer >>
var contactComponent = order.GetComponent<ContactComponent>();
var getCustomerCommand = new GetCustomerCommand(this.getCustomerPipeline, this.serviceProvider);
var customerId = $"{CommerceEntity.IdPrefix<Customer>()}{contactComponent.CustomerId}";
var customer = await getCustomerCommand.Process(context.CommerceContext, customerId);
// The order will be created against a default "Anonymous customer" if its not found
if (customer != null)
{
//!!!!REPLACE THIS WITH YOU CALL TO YOUR ERP, use the erpPolicy variable to retreive the connection details!!!!!
var customerIntegrated = true;
customer.GetComponent<ErpCustomerStatusComponent>().StatusDate = DateTime.Now;
customer.GetComponent<ErpCustomerStatusComponent>().Integrated = customerIntegrated;
// Update the order status on success
if (customerIntegrated)
{
customer.GetComponent<ErpCustomerStatusComponent>().ErpUrl = erpPolicy.AosUrl;
customer.GetComponent<ErpCustomerStatusComponent>().StatusMessage = "Success";
//!!!!REPLACE THIS WITH THE CUSTOMER ID OF YOUR ERP!!!!!!!
customer.GetComponent<ErpCustomerStatusComponent>().ErpCustomerNo = "Cust-" + contactComponent.CustomerId;
}
else
{
customer.GetComponent<ErpCustomerStatusComponent>().ErpUrl = erpPolicy.AosUrl;
customer.GetComponent<ErpCustomerStatusComponent>().StatusMessage =
"There was an error while sending the data to ERP";
}
// Update the customer
var persistCustomerResult = await this.persistEntityPipeline.Run(new PersistEntityArgument(customer), context);
}
// *** Create customer <<
return customer;
}
}
}

Creating a new pipeline block to send the order to the ERP and update the status

The final step, now we can add code that will run during the execution of the released orders minion and send the orders to our ERP.

We do this by adding a pipeline block that runs after all the other code is executed by the “Released orders Minion” pipeline.

Adding a new pipeline block

  1. In your “Sitecore.Commerce.Connectors.MYERP.Plugin” project add a new class named “SendOrderToErpMinionBlock.cs” in your “Pipelines\blocks” folder.
  2. Copy the following code over the new class, you may need to change namespaces etc.


namespace Sitecore.Commerce.Connectors.MYERP.Plugin.Pipelines.Blocks
{
using System;
using System.Threading.Tasks;
using Sitecore.Commerce.Connectors.MYERP.Plugin.Erp;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Plugin.Customers;
using Sitecore.Commerce.Plugin.Orders;
using Sitecore.Framework.Conditions;
using Sitecore.Framework.Pipelines;
/// <summary>
/// The process orders minion block.
/// </summary>
public class SendOrderToErpMinionBlock : PipelineBlock<Order, Order, CommercePipelineExecutionContext>
{
/// <summary>
/// The _persistEntityPipeline.
/// </summary>
private readonly IPersistEntityPipeline _persistEntityPipeline;
/// <summary>
/// The _find entity pipeline.
/// </summary>
private readonly IFindEntityPipeline _findEntityPipeline;
/// <summary>
/// The service provider.
/// </summary>
private readonly IServiceProvider serviceProvider;
/// <summary>
/// The get customer pipeline.
/// </summary>
private readonly IGetCustomerPipeline getCustomerPipeline;
/// <summary>
/// Initializes a new instance of the <see cref="SendOrderToErpMinionBlock"/> class.
/// </summary>
/// <param name="persistEntityPipeline">
/// The persist entity pipeline.
/// </param>
/// <param name="findEntityPipeline">
/// The find entity pipeline.
/// </param>
/// <param name="getCustomerPipeline">
/// The get customer pipeline.
/// </param>
/// <param name="serviceProvider">
/// The service provider.
/// </param>
public SendOrderToErpMinionBlock(IPersistEntityPipeline persistEntityPipeline, IFindEntityPipeline findEntityPipeline, IGetCustomerPipeline getCustomerPipeline, IServiceProvider serviceProvider)
{
this._persistEntityPipeline = persistEntityPipeline;
this._findEntityPipeline = findEntityPipeline;
this.getCustomerPipeline = getCustomerPipeline;
this.serviceProvider = serviceProvider;
}
/// <summary>
/// The run.
/// </summary>
/// <param name="arg">
/// The argument.
/// </param>
/// <param name="context">
/// The context.
/// </param>
/// <returns>
/// The <see cref="Task"/>.
/// </returns>
public override async Task<Order> Run(Order arg, CommercePipelineExecutionContext context)
{
Condition.Requires(arg).IsNotNull("The argument can not be null");
var integrationHelper = new IntegrationHelper(this._persistEntityPipeline, this._findEntityPipeline, this.serviceProvider, this.getCustomerPipeline);
var returnedOrder = await integrationHelper.TranslateOrderAndSendToErp(arg, context);
return returnedOrder;
}
}
}

Register the new pipeline block to execute during the “Released order Minion pipeline”

In order for the engine to know about our custom code, we need to register our new block by following these steps

  1. In your “Sitecore.Commerce.Connectors.MYERP.Plugin” project, copy the following code over the ConfigureSitecore.cs class, you may need to change namespaces etc.


namespace Sitecore.Commerce.Connectors.MYERP.Plugin
{
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Sitecore.Commerce.Connectors.MYERP.Plugin.Pipelines.Blocks;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Plugin.Orders;
using Sitecore.Framework.Configuration;
using Sitecore.Framework.Pipelines.Definitions.Extensions;
/// <summary>
/// The configure site core.
/// </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(
pipeLineConfig =>
pipeLineConfig.ConfigurePipeline<IReleasedOrdersMinionPipeline>(
config => config.Add<SendOrderToErpMinionBlock>()
.After<MoveReleasedOrderBlock>()));
services.RegisterAllCommands(assembly);
}
}
}

Running and debugging the example

Verify your solution, should look like this…

example solution

Environment setup

When you start the solution in Visual studio it will start a new IIS Express instance and launch your customised engine on port 5000. If you are using a single machine to do development with the demo environment already installed you will need to stop the deafult Commerce site running in IIS on port 5000. This is because they will clash and you may get an “access is denied” error when you start IIS express.

To run the customised engine instead of the default demo commerce engine follow these steps :-

  1. Stop your IIS site running Sitecore commerce. If you have a demo site the site is usually called “CommerceAuthoring” and operates on port 5000.
  2. Hit start in Visual studio on your “Customer.Sample.Solution” solution. IIS express should start and because its running on the same port as the default demo commerce engine the reference storefront and Postman will make calls to your customised engine.
  3. To revert back to the default demo commerce engine just stop your solution and start the commerce engine site hosted in in IIS.

Stop commerce site

Verify that your new plugin is loaded

  1. Make sure your solution is built and has been started at least once.
  2. Open your latest node configuration log by right clicking on the “Customer.Sample.Solution”.
  3. Select open “Open folder in file explorer”
  4. Browse to the “log” directory under “wwwroot” e.g. “C:\Development\Sitecore.Commerce.SDK.1.1.819\Sitecore.Commerce.Engine\wwwroot\logs”
  5. Open the latest “.log” file starting with “NodeConfiguration” for example “NodeConfiguration_Deployment01_e9386fa363404dd8bbbfc35f560e8728.log”
  6. Search for “MYERP” in the file, or another value that will uniquely identify your plug-in.
  7. You should see your plugin block registed against the “Release order minion” pipeline. Something like this…

registered block

Create a test order and run the modified minion pipeline

Now that everything is in place you can create an order and run your code.

  1. Add a break point to your “SendOrderToErpMinionBlock” class on line 1 of the “Run” method.
  2. Start IIS express from the solution in Visual studio.
  3. Browse to the reference store front, add items to the cart and complete the order.
  4. The order will be created as “Pending”
  5. Go to postman and run the API call for “Run PendingOrders Minion” (mention earlier).
  6. You may have to wait 5 minutes and your order will change to “Released”.
  7. Run the API call for “Run ReleasedOrders Minion”, your break point should now be hit and you can debug your code.
  8. If you let the code run all the way through your order status should change to “Complete – Sent to ERP”.

Conclusion

This example was created to show one approach to sending orders and customers to an ERP. However, it can also be used as a reference on how to customise certain aspects of the commerce engine.

In other posts I plan to show the following.

  1. How to create a command (API call) to resend the order if it failed
  2. Modifications to the business tools to show order and customer integration status.
  3. Custom entity lists which contain failed and successfully integrated orders.