Selling Customizable Products with Sitecore Commerce Engine
With Sitecore Commerce and Sitecore Commerce Engine, you have a catalog that contains products which may or may not be under categories.
The product may or may not have variants. What if you have a client which product variants are not predefined or are so diverse be cause their business model is based on the customers defining the variant at the time of ordering the product.
A few examples are:
An online flower retailer that allows customers to add greetings along with the flower and also specify the message on the flower vase.
A car manufacturer or retailer that allows customers customize the car by selecting color, sound system, wheel type, interior decor and the like at the time of order.
A home builder that allows customweers to design their homes online before ordering.
A tee shirt retailer that allows customers to design the tee-shirt with the messages on it online.
Another example is a greetings card business where customers design their own external and internal card appearance and messages.
Other examples are products that a priced by size, volume or weight and the customer can specify these parameters before adding to cart.
There could also be complex services sold by insurance or consulting firms that a dictated by the customers at the time of order.
The question is how can we use Sitecore Commerce Engine in selling these type of customizable products or services.
While it is not easy to capture these variations in a catalog, it is possible to sell them with ease by adding the right plugin.
Since users are going to be creating the variant at the point of order, the place where they will be doing that will most likely be the PDP(Product Detail Page) .
The PDP should have the wright javascript widget or interface that will allow the customer to define the variation of the product of services the will like to purchase. Once done, and the customer attempts to add to cart, the variation should be also sent to the backend along with the usual product parameters for adding a line to cart.
The parameters should now be added to the carline request as a collection of properties.
It could then be added to the cartline after the czartline has been sent to the commerce engine.
Here is how we can create a plugin in Sitecore Commerce Engine to this solutoin.
Create a new .Net Core project in your Sitecore Commerce Engine solution and name it as you wish. I will be naming the one for this blog
"Sitecore.Commerce.Plugin.CustomizableProduct"
IMAGE
Replace the content of the project.json file with the following:
{
"version": "1.0.1",
"dependencies": {
"Sitecore.Commerce.Core": "1.2.*",
"Sitecore.Commerce.Plugin.CsAgent": "1.2.159",
"Sitecore.Commerce.Plugin.Fulfillment": "1.2.159",
"Sitecore.Commerce.Plugin.Management": "1.2.159",
"Sitecore.Commerce.Plugin.Catalog": "1.2.159",
"Sitecore.Commerce.Plugin.Catalog.Cs": "1.2.159"
},
"frameworks": {
"net461": {
}
}
}
Create the following folders:
- Commands
- Components
- Controller
- Pipelines
- Pipelines/Blocks
- Models
In model, create the following classes:
KeyValue.cs
public class KeyValue
{
///
/// Key
///
public string Key { get; set; }
///
/// Value
///
public object Value { get; set; }
}
Properties.cs
public class Properties
{
///
/// List of ketvalues
///
public List KeyValues { get; set; }
}
CartLineProperty.cs
public class CartLineProperty
{
///
///
///
public string CartLineId { get; set; }
///
///
///
public Properties Properties { get; set; }
}
CartLineProperties.cs
public class CartLineProperties
{
///
///
///
public List CartLineProperty { get; set; }
}
Under component folder, create:
CartComponent.cs
public class CartComponent : Component
{
///
///
///
public Models.Properties Properties { get; set; }
}
Under Commands folder, create:
SetCartLinePropertiesCommand.cs
public class SetCartLinePropertiesCommand : CommerceCommand
{
///
///
///
private readonly IPersistEntityPipeline _persistEntityPipeline;
///
///
///
///
///
public SetCartLinePropertiesCommand(IServiceProvider serviceProvider,
IPersistEntityPipeline persistEntityPipeline) : base(serviceProvider)
{
_persistEntityPipeline = persistEntityPipeline;
}
public async Task Process(CommerceContext commerceContext, string cartId, CartLineProperties lineProperties, string baseUrl)
{
try
{
var cart = GetCart(cartId, commerceContext, baseUrl);
if (cart == null)
{
return null;
}
// Set the custom fields on the cartlines
if (cart.Lines != null && cart.Lines.Any() && lineProperties != null &&
lineProperties.CartLineProperty.Any())
{
foreach (var cartLineProperty in lineProperties.CartLineProperty)
{
var cartLineComponent = cart.Lines.FirstOrDefault(x => x.Id == cartLineProperty.CartLineId);
if (cartLineComponent != null)
cartLineComponent
.GetComponent().Properties = cartLineProperty.Properties;
}
}
var result = await this._persistEntityPipeline.Run(new PersistEntityArgument(cart), commerceContext.GetPipelineContextOptions());
return result.Entity as Cart;
}
catch (Exception e)
{
return await Task.FromException(e);
}
}
private Cart GetCart(string cartId, CommerceContext commerceContext, string baseUrl)
{
var shopName = commerceContext.CurrentShopName();
var shopperId = commerceContext.CurrentShopperId();
var customerId = commerceContext.CurrentCustomerId();
var environment = commerceContext.Environment.Name;
var url = string.Format(Constants.Settings.EndpointUrl, baseUrl, cartId);
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Settings.AppJson));
client.DefaultRequestHeaders.Add(Constants.Settings.ShopName, shopName);
client.DefaultRequestHeaders.Add(Constants.Settings.ShopperId, shopperId);
client.DefaultRequestHeaders.Add(Constants.Settings.Language, "en-US");
client.DefaultRequestHeaders.Add(Constants.Settings.Environment, environment);
client.DefaultRequestHeaders.Add(Constants.Settings.CustomerId, customerId);
client.DefaultRequestHeaders.Add(Constants.Settings.Currency, commerceContext.CurrentCurrency());
client.DefaultRequestHeaders.Add(Constants.Settings.Roles, Constants.Settings.CartRoles);
try
{
var cart = new Cart();
var response = client.GetAsync(url).Result;
if (response != null)
{
var task = response.Content.ReadAsStreamAsync().ContinueWith(t =>
{
var stream = t.Result;
using (var reader = new StreamReader(stream))
{
var responseValue = reader.ReadToEnd();
cart = JsonConvert.DeserializeObject(responseValue);
}
});
task.Wait();
}
client.Dispose();
return cart;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
client.Dispose();
return null;
}
}
}
Under Controller folder, create:
CommandsController.cs
public class CommandsController : CommerceController
{
///
///
///
///
///
public CommandsController(IServiceProvider serviceProvider, CommerceEnvironment globalEnvironment) : base(serviceProvider, globalEnvironment)
{
}
//
[HttpPost]
[Route("SetCartLineProperties()")]
public async Task SetCartLineProperties([FromBody] ODataActionParameters value)
{
if (!this.ModelState.IsValid)
return (IActionResult)new BadRequestObjectResult(this.ModelState);
if (!value.ContainsKey(Constants.Settings.CartId)) return (IActionResult)new BadRequestObjectResult((object)value);
var id = value[Constants.Settings.CartId];
if (string.IsNullOrEmpty(id?.ToString())) return (IActionResult)new BadRequestObjectResult((object)value);
var cartId = id.ToString();
var cartLineProperties = new CartLineProperties();
if (value.ContainsKey(Constants.Settings.CartLineProperties))
{
var cartlinePropObj = value[Constants.Settings.CartLineProperties];
if (!string.IsNullOrEmpty(cartLineProperties?.ToString()))
{
cartLineProperties = JsonConvert.DeserializeObject(cartlinePropObj.ToString());
}
}
var command = this.Command();
await Task.Delay(1);
var runCommand = await command.Process(this.CurrentContext, cartId, cartLineProperties, $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}");
return (IActionResult)new ObjectResult((object)runCommand);
}
}
Under Pipelines/Blocks, create:
ConfigureServiceApiBlock.cs
public class ConfigureServiceApiBlock : PipelineBlock
{
private readonly IPersistEntityPipeline _persistEntityPipeline;
public ConfigureServiceApiBlock(IPersistEntityPipeline persistEntityPipeline)
{
_persistEntityPipeline = persistEntityPipeline;
}
public override Task Run(ODataConventionModelBuilder modelBuilder, CommercePipelineExecutionContext context)
{
Condition.Requires(modelBuilder).IsNotNull($"{base.Name}: The argument can not be null");
var cartLineConfiguration = modelBuilder.Action("SetCartLineProperties");
cartLineConfiguration.Parameter("cartId");
cartLineConfiguration.Parameter("properties");
cartLineConfiguration.ReturnsFromEntitySet("Commands");
return Task.FromResult(modelBuilder);
}
}
At the root of the project create the folowing classes:
Constants.cs
public class Constants
{
public struct Settings
{
public const string EndpointUrl = "{0}/api/Carts('{1}')?$expand=Lines($expand=CartLineComponents($expand=ChildComponents)),Components($expand=ChildComponents)";
public const string AppJson = "application/json";
public const string ShopperId = "ShopperId";
public const string CartRoles = "sitecore\\Pricer Manager|sitecore\\Promotioner Manager";
public const string CartId = "cartId";
public const string CartLineProperties = "cartLineProperties";
public const string ShopName = "ShopName";
public const string Language = "Language";
public const string Environment = "Environment";
public const string GeoLocation = "GeoLocation";
public const string CustomerId = "CustomerId";
public const string Currency = "Currency";
public const string Roles = "Roles";
}
}
ConfigureSitecore.cs
public class ConfigureSitecore : IConfigureSitecore
{
public void ConfigureServices(IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
services.RegisterAllPipelineBlocks(assembly);
Action actionDelegate = c => c
.ConfigurePipeline(
d =>
{
d.Add();
});
services.Sitecore().Pipelines(actionDelegate);
// Register commands too.
services.RegisterAllCommands(assembly);
}
}
To install this plugin, open your base storefront plugin which may be Sitecore.Commerce.Plugin.Adventureworks and add the line below to the project.json file under Dependencies.
"Sitecore.Commerce.Plugin.CustomizableProduct": "1.0.1"
We now need a code for the Sitecore project side that will send custom order propeerrties to Sitecore Commerce Engine's controller.
Under you Sitecore solution
Create a class named "CommerceEngineConfiguration" for mapping the commerce config to a model.
public class CommerceEngineConfiguration
{
public string ShopsServiceUrl { get; set; }
public string DefaultEnvironment { get; set; }
public string DefaultShopName { get; set; }
public string DefaultShopCurrency { get; set; }
public string CertificateValidationEnabled { get; set; }
public object CertificateThumbprint { get; set; }
}
, create another class and name it as you wish. I will be naming the one in this blog "SetSubmitCartlineCustomPropertiesByCommand"
Add the following code to the class:
public class SetSubmitCartlineCustomPropertiesByCommand : PipelineProcessor
{
///
/// The process.
///
///
/// The args.
///
public override void Process(ServicePipelineArgs args)
{
var request = args.Request as AddCartLinesRequest;
var result = args.Result as CartResult;
try
{
if (request != null)
{
Assert.IsNotNull(request.Cart, Constants.Cart.RequestDotCart);
Assert.IsNotNullOrEmpty(request.Cart.UserId, Constants.Cart.RequestDotCartUid);
Assert.IsNotNull(request.Cart.Lines, Constants.Cart.RequestDotCartLines);
Assert.IsNotNull(request.Lines, Constants.Cart.RequestDotLines);
CallRemote(request);
}
}
catch (ArgumentException exception)
{
if (result != null)
{
result.Success = false;
result.SystemMessages.Add(this.CreateSystemMessage(exception));
}
}
catch (AggregateException exception2)
{
if (result != null)
{
result.Success = false;
result.SystemMessages.Add(this.CreateSystemMessage(exception2));
}
}
}
private void CallRemote(AddCartLinesRequest request)
{
var cart = request.Cart;
var cartId = $"{cart.Name}{cart.CustomerId}{cart.ShopName}')";
CommerceEngineConfiguration commerceEngineConfiguration = null;
try
{
var commerceConfigStr = Sitecore.Configuration.Factory.GetConfigNode(Constants.Cart.CommerceEngineConfiguration).OuterXml;
var doc = new XmlDocument();
doc.LoadXml(commerceConfigStr);
var json = JsonConvert.SerializeXmlNode(doc);
commerceEngineConfiguration = JsonConvert.DeserializeObject(json);
}
catch (Exception ex)
{
Log.Error(ex.Message, ex, this);
}
if (commerceEngineConfiguration != null)
{
var url = $"{commerceEngineConfiguration.ShopsServiceUrl}{Constants.Cart.SetCartLineProperties}";
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Cart.AppJson));
client.DefaultRequestHeaders.Add(Constants.Cart.ShopName, cart.ShopName);
client.DefaultRequestHeaders.Add(Constants.Cart.ShopperId, Constants.Cart.ShopperId);
client.DefaultRequestHeaders.Add(Constants.Cart.Language, "en-US");
client.DefaultRequestHeaders.Add(Constants.Cart.Environment, commerceEngineConfiguration.DefaultEnvironment);
client.DefaultRequestHeaders.Add(Constants.Cart.GeoLocation, "1.0.0.0");
client.DefaultRequestHeaders.Add(Constants.Cart.CustomerId, cart.CustomerId);
client.DefaultRequestHeaders.Add(Constants.Cart.Currency, cart.CurrencyCode.ToUpper());
client.DefaultRequestHeaders.Add(Constants.Cart.Roles, Constants.Cart.CartRoles);
var cartLineProperties = new CartLineProperties();
// Create a list of all properties on cartlines
if (cart.Lines != null && cart.Lines.Any())
{
var cartlinePropertiesList = new List();
foreach (var cartLine in cart.Lines)
{
var newCartProperties = new Properties();
var kvList = new List();
if (cartLine.Properties != null && cartLine.Properties.Any())
{
var setProperties = cartLine.Properties.ToList();
foreach (var propertyItem in setProperties)
{
var keyvalue = new KeyValue
{
Key = propertyItem.Key,
Value = propertyItem.Value
};
kvList.Add(keyvalue);
}
newCartProperties.KeyValues = kvList;
var cartLineProperty = new CartLineProperty
{
CartLineId = cartLine.ExternalCartLineId,
Properties = newCartProperties
};
cartlinePropertiesList.Add(cartLineProperty);
}
}
cartLineProperties.CartLineProperty = cartlinePropertiesList;
}
var cartLinePropertiesJson = new JavaScriptSerializer().Serialize(cartLineProperties);
var values = new
{
cartId = cartId,
cartLineProperties = cartLinePropertiesJson,
};
var jsonRequest = new JavaScriptSerializer().Serialize(values);// converting the obj into a JSON object
var content = new StringContent(jsonRequest, Encoding.UTF8, Constants.Cart.TextJson);
try
{
var response = client.PostAsync(url, content).Result;
}
catch (Exception e)
{
Log.Error(e.Message, e, this);
}
client.Dispose();
}
}
///
/// The create system message.
///
///
/// The ex.
///
///
/// The .
///
public SystemMessage CreateSystemMessage(Exception ex)
{
var message = new SystemMessage
{
Message = ex.Message
};
if ((ex.InnerException != null) && !ex.Message.Equals(ex.InnerException.Message, StringComparison.OrdinalIgnoreCase))
{
message.Message = message.Message + " - " + ex.InnerException.Message;
}
return message;
}
}
Create a config file and add the following config to send custom cartline properties to commerce engine after a cartline has been added.
Now, to use this in your project, after add cartline to cart request preparation:
Set the properties that correspond to the choices the customer made on the PDP
var cart = cartResult.Cart as CommerceCart;
var addLinesRequest = new AddCartLinesRequest(cart, lines);
addLinesRequest.RefreshCart(true);
// Set the custom properties here, get them from a cookie or any other mechanism
addLinesRequest.Properties\["option1"\] = "option1";
addLinesRequest.Properties\["option2"\] = "option2";
addLinesRequest.Properties\["option3"\] = "option3";
var addLinesResult = this.CartServiceProvider.AddCartLines(addLinesRequest);
Aftrer this line, our custom options are sent to commerce engine and are saved on the cartline.
At that point, commerce Engine code can use the options to recalculate the cost of the product or service line based on the customer selections.
When the cart becomes an order, the data is retained and can be used to fulfil the order.
To download the code from github, click:
[Download](https://github.com/XCentium/SC-Plugin-CustomOrderLineJSON "Download From Github").