Its common to update and create items periodically from other applications via the commerce engine API. In most cases the data passed to the commerce engine is a lot simpler than the standard schema for the SellableItem. 

Its also common that the group of developers calling the API is unfamiliar with Sitecore commerce and does not have the time to learn the ins and outs of the schema.

For example, do the callers of the commerce API really need to know what a child component is or even that the SellableItem is the product? Generally, no, they just want to set a value on the product.

In this post we will look at how to create a simplified input JSON schema  and expose it via the API. In part 2 we will create a Azure Function and call the new API method using the proxy.

The new API method supports the following :-

  1. Creation of multiple products and their variants in the same call using a simple JSON format
  2. Specifying the list price

The simplified input will look like this…

{
"products" :
[
{
"productid": "Product1",
"listprice": "10.99",
"description": "Product 1 is the best!!!",
"variants" :
[
{
"variantid": "Product1-Variant1",
"listprice": "11.99"
},
{
"variantid": "Product1-Variant2",
"listprice": "12.99"
},
{
"variantid": "Product1-Variant3",
"listprice": "13.99"
}
]
}
]
}

The following is covered in this post :-

  • Create a new pipeline and block to handle the upsert of the sellableitem from a simplified JSON format
  • Create a new function that accepts a simplified JSON format and expose it via the API. This function will also allow the upsert of more than product at a time.
  • Test the new function via Postman 

Step 1 – Create the plumbing

  • In the Commerce Engine SDK create a new plugin and reference it from the engine
  • Add the following code to create the plumbing required to support the the API method.

In folder named “Pipelines\Arguments” create…

using Sitecore.Commerce.Core;
namespace Sitecore.Services.Examples.Catalog.Pipelines.Arguments
{
public class CreateUpdateSellableItemVariantArgument : PipelineArgument
{
public CreateUpdateSellableItemVariantArgument(string productId, string variantId)
{
ProductId = productId;
VariantId = variantId;
}
public string ProductId { get; set; }
public string VariantId { get; set; }
public decimal ListPrice { get; set; }
}
}
using System.Collections.Generic;
using Sitecore.Commerce.Core;
namespace Sitecore.Services.Examples.Catalog.Pipelines.Arguments
{
public class CreateUpdateSellableItemArgument : PipelineArgument
{
public CreateUpdateSellableItemArgument()
{
Variants = new List<CreateUpdateSellableItemVariantArgument>();
}
public List<CreateUpdateSellableItemVariantArgument> Variants { get; set; }
public string ProductId { get; set; }
public string Description { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public decimal ListPrice { get; set; }
}
}

In a folder named “Pipelines” create…

using System.Collections.Generic;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Pipelines;
using Sitecore.Services.Examples.Catalog.Pipelines.Arguments;
namespace Sitecore.Services.Examples.Catalog.Pipelines
{
[PipelineDisplayName("CreateUpdateSellableItemPipeline")]
public interface ICreateUpdateSellableItemPipeline : IPipeline<List<CreateUpdateSellableItemArgument>, bool, CommercePipelineExecutionContext>
{
}
}
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Pipelines;
using Sitecore.Services.Examples.Catalog.Pipelines.Arguments;
namespace Sitecore.Services.Examples.Catalog.Pipelines
{
public class CreateUpdateSellableItemPipeline : CommercePipeline<List<CreateUpdateSellableItemArgument>, bool>, ICreateUpdateSellableItemPipeline
{
public CreateUpdateSellableItemPipeline(IPipelineConfiguration<ICreateUpdateSellableItemPipeline> configuration, ILoggerFactory loggerFactory)
: base(configuration, loggerFactory)
{
}
}
}

In a folder named “Pipelines\Blocks” create…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Plugin.Catalog;
using Sitecore.Commerce.Plugin.Pricing;
using Sitecore.Framework.Conditions;
using Sitecore.Framework.Pipelines;
using Sitecore.Services.Examples.Catalog.Pipelines.Arguments;
namespace Sitecore.Services.Examples.Catalog.Pipelines.Blocks
{
[PipelineDisplayName("CreateUpdateSellableItem.CreateUpdateSellableItemBlock")]
public class CreateUpdateSellableItemBlock : PipelineBlock<List<CreateUpdateSellableItemArgument>, bool, CommercePipelineExecutionContext>
{
private readonly CommerceCommander _commander;
public CreateUpdateSellableItemBlock(CommerceCommander commander)
{
_commander = commander;
}
public override async Task<bool> Run(List<CreateUpdateSellableItemArgument> arg, CommercePipelineExecutionContext context)
{
Condition.Requires(arg).IsNotNull($"{Name}: The argument can not be null");
var result = new List<string>();
foreach (var sellableItemArg in arg)
{
var id = $"{CommerceEntity.IdPrefix<SellableItem>()}{sellableItemArg.ProductId}";
//Will be created if not found
var foundEntity = await _commander.Pipeline<FindEntityPipeline>().Run(new FindEntityArgument(typeof(SellableItem), id, false), context) ??
await _commander.Command<CreateSellableItemCommand>().Process(context.CommerceContext,
sellableItemArg.ProductId,sellableItemArg.Name,sellableItemArg.DisplayName,sellableItemArg.Description);
if (!(foundEntity is SellableItem sellableItem)) continue;
UpdateCreateListPrice(sellableItem.GetPolicy<ListPricingPolicy>(), sellableItemArg.ListPrice);
sellableItem.Description = sellableItemArg.Description;
sellableItem.DisplayName = sellableItemArg.DisplayName;
sellableItem.Name = sellableItemArg.Name;
foreach (var variantArgs in sellableItemArg.Variants)
{
var variant = sellableItem.GetVariation(variantArgs.VariantId);
if (variant == null)
{
variant = new ItemVariationComponent
{
Id = variantArgs.VariantId
};
sellableItem.GetComponent<ItemVariationsComponent>().ChildComponents.Add(variant);
}
UpdateCreateListPrice(variant.GetPolicy<ListPricingPolicy>(), variantArgs.ListPrice);
}
var persistResult = await _commander.Pipeline<PersistEntityPipeline>()
.Run(new PersistEntityArgument(sellableItem), context);
result.Add(persistResult.Entity.Id);
}
var ok = result.Count != 0;
return await Task.FromResult(ok);
}
private static void UpdateCreateListPrice(ListPricingPolicy pricePolicy, decimal price)
{
var money = pricePolicy.Prices.FirstOrDefault(x => x.CurrencyCode.Equals("USD", StringComparison.OrdinalIgnoreCase));
if (money == null)
{
pricePolicy.AddPrice(new Money(price));
}
else
{
money.Amount = price;
}
}
}
}

In a folder named “Commands” create…

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using Sitecore.Services.Examples.Catalog.Pipelines;
using Sitecore.Services.Examples.Catalog.Pipelines.Arguments;
namespace Sitecore.Services.Examples.Catalog.Commands
{
public class CreateUpdateSellableItemCommand : CommerceCommand
{
private readonly ICreateUpdateSellableItemPipeline _pipeline;
public CreateUpdateSellableItemCommand(ICreateUpdateSellableItemPipeline pipeline, IServiceProvider serviceProvider) : base(serviceProvider)
{
_pipeline = pipeline;
}
public async Task<bool> Process(CommerceContext commerceContext, List<CreateUpdateSellableItemArgument> args)
{
using (var activity = CommandActivity.Start(commerceContext, this))
{
var result = await _pipeline.Run(args, new CommercePipelineExecutionContextOptions(commerceContext));
return result;
}
}
}
}

Step 2 – Create the new API method

In a folder called “Controllers” create…

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web.Http.OData;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Sitecore.Commerce.Core;
using Sitecore.Services.Examples.Catalog.Commands;
using Sitecore.Services.Examples.Catalog.Pipelines.Arguments;
namespace Sitecore.Services.Examples.Catalog.Controllers
{
public class ApiController : CommerceController
{
protected internal const string CreateUpdateSellableItemStr = "createUpdateSellableItem";
protected internal const string ProductsStr = "products";
public ApiController(IServiceProvider serviceProvider, CommerceEnvironment globalEnvironment)
: base(serviceProvider, globalEnvironment)
{
}
[HttpPut]
[Route("CreateUpdateSellableItem()")]
public async Task<IActionResult> CreateUpdateSellableItem([FromBody] ODataActionParameters value)
{
if (!ModelState.IsValid)
{
return new BadRequestObjectResult(ModelState);
}
if (!value.ContainsKey(CreateUpdateSellableItemStr))
{
return new BadRequestObjectResult(value);
}
//Handling scenarios where the input is already converted into JArray (postman) or passed through as string
JArray products;
if (value[CreateUpdateSellableItemStr] is JArray == false)
{
var payload = JObject.Parse(value[CreateUpdateSellableItemStr].ToString());
products = (JArray) payload[ProductsStr];
}
else
{
products = value[CreateUpdateSellableItemStr] as JArray;
}
var sellableItemArguments = new List<CreateUpdateSellableItemArgument>();
foreach (var product in products)
{
var sellableItemArgument = new CreateUpdateSellableItemArgument();
var productid = product["productid"].ToString();
sellableItemArgument.ProductId = productid;
var description = product["description"].ToString();
sellableItemArgument.Description = description;
var name = product["name"].ToString();
sellableItemArgument.Name = name;
var displayName = product["displayname"].ToString();
sellableItemArgument.DisplayName = displayName;
var listprice = product["listprice"].ToString();
var isDecimal = decimal.TryParse(listprice, out var n);
if (isDecimal)
{
sellableItemArgument.ListPrice = Convert.ToDecimal(listprice);
}
var variants = (JArray) product["variants"];
foreach (var variant in variants)
{
var variantArg = new CreateUpdateSellableItemVariantArgument(sellableItemArgument.ProductId, variant["variantid"].ToString());
var variantListPrice = variant["listprice"].ToString();
if (decimal.TryParse(variantListPrice, out var outPrice))
{
variantArg.ListPrice = Convert.ToDecimal(variantListPrice);
}
sellableItemArgument.Variants.Add(variantArg);
}
sellableItemArguments.Add(sellableItemArgument);
}
var result = await Command<CreateUpdateSellableItemCommand>().Process(CurrentContext, sellableItemArguments);
return new ObjectResult(result);
}
}
}

In the root of the project create…

using Sitecore.Commerce.Core.Commands;
using Sitecore.Commerce.Plugin.Catalog;
namespace Sitecore.Services.Examples.Catalog
{
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Builder;
using Commerce.Core;
using Framework.Conditions;
using Sitecore.Framework.Pipelines;
[PipelineDisplayName("ConfigureServiceApiBlock")]
public class ConfigureServiceApiBlock : PipelineBlock<ODataConventionModelBuilder, ODataConventionModelBuilder, CommercePipelineExecutionContext>
{
public override Task<ODataConventionModelBuilder> Run(ODataConventionModelBuilder modelBuilder, CommercePipelineExecutionContext context)
{
Condition.Requires(modelBuilder).IsNotNull($"{Name}: The argument cannot be null.");
var createUpdateSellableItem = modelBuilder.Action("CreateUpdateSellableItem");
createUpdateSellableItem.Parameter<string>("createUpdateSellableItem");
createUpdateSellableItem.Returns<bool>();
return Task.FromResult(modelBuilder);
}
}
}
using Sitecore.Services.Examples.Catalog.Pipelines;
using Sitecore.Services.Examples.Catalog.Pipelines.Blocks;
namespace Sitecore.Services.Examples.Catalog
{
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Commerce.Core;
using Framework.Configuration;
using Sitecore.Framework.Pipelines.Definitions.Extensions;
using Commerce.EntityViews;
using Sitecore.Commerce.Plugin.Catalog;
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
.AddPipeline<ICreateUpdateSellableItemPipeline, CreateUpdateSellableItemPipeline>(
configure =>
{
configure.Add<CreateUpdateSellableItemBlock>();
})
.ConfigurePipeline<IConfigureServiceApiPipeline>(configure => configure.Add<ConfigureServiceApiBlock>()));
services.RegisterAllCommands(assembly);
}
}
}

Your project should look like this…

Step 3 – Test with Postman

Deploy…

  • Build the solution
  • Start the engine in console mode

Test…

  • Place a break point in the ApiController.CreateUpdateSellableItem method
  • Open postman and import the example postman code from this JSON…
{
"info": {
"_postman_id": "4ea2f3c9-d8e0-4ca0-95c2-acf232ee5ef2",
"name": "Examples",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "InventoryUpdate",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "ShopName",
"value": "{{ShopName}}"
},
{
"key": "ShopperId",
"value": "{{ShopperId}}"
},
{
"key": "Language",
"value": "{{Language}}"
},
{
"key": "Currency",
"value": "{{Currency}}"
},
{
"key": "Environment",
"value": "{{Environment}}"
},
{
"key": "GeoLocation",
"value": "{{GeoLocation}}"
},
{
"key": "CustomerId",
"value": "{{CustomerId}}"
},
{
"key": "Authorization",
"value": "{{SitecoreIdToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"inventory\" :\r\n [\r\n {\r\n \"productId\": \"\",\r\n \"size\": \"\",\r\n \"uom\": \"\"\r\n },\r\n {\r\n \"productId\": \"\",\r\n \"size\": \"\",\r\n \"uom\": \"\"\r\n }\r\n ]\r\n}\r\n"
},
"url": {
"raw": "https://{{ServiceHost}}/{{ShopsApi}}/InventoryUpdate",
"protocol": "https",
"host": [
"{{ServiceHost}}"
],
"path": [
"{{ShopsApi}}",
"InventoryUpdate"
]
}
},
"response": []
},
{
"name": "SellableItemUpdate",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "ShopName",
"value": "{{ShopName}}"
},
{
"key": "ShopperId",
"value": "{{ShopperId}}"
},
{
"key": "Language",
"value": "{{Language}}"
},
{
"key": "Currency",
"value": "{{Currency}}"
},
{
"key": "Environment",
"value": "{{Environment}}"
},
{
"key": "GeoLocation",
"value": "{{GeoLocation}}"
},
{
"key": "CustomerId",
"value": "{{CustomerId}}"
},
{
"key": "Authorization",
"value": "{{SitecoreIdToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"products\" :\r\n [\r\n {\r\n \"productid\": \"Product1\",\r\n \"listprice\": \"10.99\",\r\n \"description\": \"Product 1 is the best!!!\",\r\n \"variants\" :\r\n \t\t[\r\n \t\t\t{\r\n \t\t\t\t\"variantid\": \"Product1-Variant1\",\r\n \t\t\t\t\"listprice\": \"11.99\"\r\n \t\t\t},\r\n \t\t\t{\r\n \t\t\t\t\"variantid\": \"Product1-Variant2\",\r\n \t\t\t\t\"listprice\": \"12.99\"\r\n \t\t\t},\r\n \t\t\t{\r\n \t\t\t\t\"variantid\": \"Product1-Variant3\",\r\n \t\t\t\t\"listprice\": \"13.99\"\r\n \t\t\t}\r\n \t\t]\r\n }\r\n ]\r\n}\r\n"
},
"url": {
"raw": "https://{{ServiceHost}}/{{ShopsApi}}/CreateUpdateSellableItem",
"protocol": "https",
"host": [
"{{ServiceHost}}"
],
"path": [
"{{ShopsApi}}",
"CreateUpdateSellableItem"
]
}
},
"response": []
}
]
}
  • Once the example postman call is created from the JSON above, you should be able to execute the postman call and your break point will be hit. You can step through the process until the block that handles the upsert of the product is run.
  • The example Postman command should look like the image at the top of this post.