Skip to content

Sitecore Commerce – Adding Custom Fields to the Shopping Cart

While working on a project using Sitecore commerce 9.3, we came across a requirement that requires a custom field called “EORI number” (Economic Operators Registration and Identification) to be saved on the commerce cart & the value is supplied from the storefront checkout shipping step. To save the EORI Number we will create & set a component that has a list of key value pairs property, so that we can save other custom fields/properties later if needed. We also need to get this property back from the commerce cart to the storefront side at a later point.

To achieve all this we need to make changes in 3 high level areas:

1) Commerce plugin:

  • Create a custom component that has a property to hold the key value pairs.
  • Create a commerce command to set the custom component on the commerce cart entity.
  • Create commands controller & configure api block to expose an endpoint to call the commerce command to set the custom component.

2) Commerce service proxy:

  • We need to rebuild the proxy to be able to access our plugin’s command controller endpoints on the storefront side.

3) Storefront/site:

  • We need to create a processor from “TranslateCartToEntity” processor to get the custom cart properties from the commerce cart (Sitecore.Commerce.Plugin.carts.Cart) & map those to the “Properties” (Dictionary type) field/property available (by default) on the connect cart entity. (Sitecore.Commerce.Engine.Connect.Entities.CommerceCart).

Detailed Steps

1.Commerce Plugin:

  • Create a component with a property to store the custom properties. We cannot use a dictionary type or list of key value pairs for the custom component property, as there will be serialization errors when serializing/deserializing the commerce cart Sitecore.Commerce.Plugin.carts.Cart).
public class CustomCartPropertiesComponent : Component
{
    public CustomCartPropertiesComponent ( )
    {
        Properties = new List <CartAttribute > ( ) ;
    }
    public List <CartAttribute > Properties { get ; set ; }
}

public class CartAttribute
{
    public string Key { get ; set ; }
    public string Value { get ; set ; }
}
  • Create a command to set the custom component on the commerce cart. This command takes the cart id & custom properties dictionary. In the command, we get the commerce cart & set the component with custom cart properties on it.
public class SetCustomCartPropertiesComponentCommand : CommerceCommand
{
    private readonly IGetCartPipeline _getCartPipeline ;
    private readonly IPersistEntityPipeline _persistEntityPipeline ;
    private readonly IUpdateCartLinePipeline _updateCartLinePipeline ;

    public SetCustomCartPropertiesComponentCommand (IGetCartPipeline       getCartPipeline, IPersistEntityPipeline persistEntityPipeline, IUpdateCartLinePipeline updateCartLinePipeline )
    {
        _getCartPipeline = getCartPipeline ;
        _persistEntityPipeline = persistEntityPipeline ;
        _updateCartLinePipeline = updateCartLinePipeline ;
    }

    public async Task <Sitecore . Commerce . Plugin . Carts . Cart > Process (CommerceContext commerceContext, string cartId, Dictionary < string, string > customCartProperties )
    {
        try
        {
            var pipeLineContextOptions = commerceContext . PipelineContextOptions ;
            var cart = await this ._getCartPipeline . Run ( new ResolveCartArgument (commerceContext . CurrentShopName ( ), cartId, commerceContext . CurrentShopperId ( ) ),
                                                            pipeLineContextOptions ) ;
            if (cart == null )
            {
                return null ;
            }

            var customCartPropertiesComponent = cart . GetComponent <CustomCartPropertiesComponent > ( ) ?? new CustomCartPropertiesComponent ( ) ;
            var properties = customCartPropertiesComponent . Properties ;
            foreach ( var entry in customCartProperties )
            {
                var currentAttr = properties . FirstOrDefault (p => p . Key == entry . Key ) ;
                if (currentAttr == null && ! string . IsNullOrWhiteSpace (entry . Value ) )
                {
                    customCartPropertiesComponent . Properties . Add ( new CartAttribute { Key = entry . Key, Value = entry . Value } ) ;
                }
                else
                {
                    if ( string . IsNullOrWhiteSpace (entry . Value ) )
                    {
                        customCartPropertiesComponent . Properties . Remove (currentAttr ) ;
                    }
                    else
                    {
                        currentAttr . Value = entry . Value ;
                    }
                }
            }
            cart . SetComponent (customCartPropertiesComponent ) ;
            var result = await this ._persistEntityPipeline . Run ( new PersistEntityArgument (cart ), pipeLineContextOptions ) ;
            return result . Entity as Sitecore . Commerce . Plugin . Carts . Cart ;
        }
        catch (Exception e )
        {
            return await Task . FromException <Sitecore . Commerce . Plugin . Carts . Cart > (e ) ;
        }
    }
}
  • Create a commands controller to expose an endpoint to set our component on the commerce cart.
public class CommandsController : CommerceController
{
    public CommandsController (IServiceProvider serviceProvider, CommerceEnvironment globalEnvironment )
        : base (serviceProvider, globalEnvironment )
    {
       
    }

    [HttpPut ]
    [Route ( "SetCustomCartProperties()" ) ]
    public async Task <IActionResult > SetCustomCartProperties ( [FromBody ] ODataActionParameters value )
    {
        var cartId = value [ "cartId" ] ?. ToString ( ) ;
        var customPropertiesString = value [ "customCartProperties" ] ?. ToString ( ) ;
        if ( string . IsNullOrWhiteSpace (cartId ) || string . IsNullOrWhiteSpace (customPropertiesString ) )
        {
            return new ObjectResult ( null ) ;
        }
        var customProperties = JsonConvert . DeserializeObject <Dictionary < string, string >> (customPropertiesString ) ;
        var setCustomCartPropertiesComponentCommand = this . Command <SetCustomCartPropertiesComponentCommand > ( ) ;
        await setCustomCartPropertiesComponentCommand . Process ( this . CurrentContext, cartId, customProperties ) ;
        return new ObjectResult (setCustomCartPropertiesComponentCommand ) ;
    }
}
  • Create a configure api block to register the route for our new command. We are registering our action with string type parameters for both cartId & custom cart properties. We are using string type for custom cart properties because we were unsuccessful when using the v6.19 OData client with “customCartProperties” parameter set to dictionary type or list of key value pairs.
public class ConfigureServiceApiBlock : PipelineBlock <ODataConventionModelBuilder, ODataConventionModelBuilder, CommercePipelineExecutionContext >
{
    public override Task <ODataConventionModelBuilder > Run (ODataConventionModelBuilder arg, CommercePipelineExecutionContext context )
    {
        Condition . Requires (arg ) . IsNotNull ($ "{Name}: The argument cannot be null." ) ;

        string entitySetName = "Commands" ;

        ActionConfiguration actionConfiguration = arg . Action ( "SetCustomCartProperties" ) ;
        actionConfiguration . Parameter < string > ( "cartId" ) ;
        actionConfiguration . Parameter < string > ( "customCartProperties" ) ;
        actionConfiguration . ReturnsFromEntitySet <CommerceCommand > (entitySetName ) ;

        return Task . FromResult (arg ) ;
    }
}
  • Create/modify existing “ConfigureSitecore” to configure the “IConfigureServiceApiPipeline” by adding the “ConfigureServiceApiBlock”
public class ConfigureSitecore : IConfigureSitecore
{
    public void ConfigureServices (IServiceCollection services )
    {
        var assembly = Assembly . GetExecutingAssembly ( ) ;
        services . RegisterAllPipelineBlocks (assembly ) ;

        services . Sitecore ( ) . Pipelines (
            config =>
                config
                    . ConfigurePipeline <IConfigureServiceApiPipeline > (
                       configure =>
                        {
                           configure . Add <ConfigureServiceApiBlock > ( ) ;
                        } )
                ) ;
        services . RegisterAllCommands (assembly ) ;
    }
}

2. Rebuild the sitecore commerce proxy:

We need to rebuild the Sitecore commerce proxy to include the new command route that we have added.The “Sitecore.Commerce.ServiceProxy.sln” can be found in the commerce engine sdk.

For Sitecore commerce 9.3:

  1. Open the “Sitecore.Commerce.ServiceProxy.sln” in visual studio 2017
  2. Make sure that the reference to the OData client is of version 6.19.

3. We need OData connected service to update the commerce ops & shops connected services. Install visual studio OData connected service extension. To use the OData client v6.19, we need to install “OData Connected Service” extension v0.3.0. This version supports visual studio 2017 only.

4. Now deploy/run the commerce engine from visual studio on default port (5000).

5. Open “Sitecore.Commerce.ServiceProxy.sln” in visual studio 2017.For each of the connected services (CommerceOps and CommerceShops), right-click the name of the service folder, and then click Update OData Connected Service.

6. We can verify if our action to set custom properties is added by going to the end point specified in the ConnectedService.json for each service.

3. Storefront/Site:

  • We can now call the commerce command using the proxy. As mentioned before, we are passing a serialized string of the custom properties dictionary because of OData client issues. This string is deserialized to the dictionary of strings on the commerce side when it hits our action
private void SetCustomCartProperties (SetShippingMethodsInputModel inputModel, Cart cart )
{
    var shippingAddress = inputModel . ShippingAddresses . FirstOrDefault ( ) ;
    if (shippingAddress != null )
    {
        var container = EngineConnectUtility . GetShopsContainer ( string . Empty,
                                                                    cart . ShopName,
                                                                    cart . UserId,
                                                                    cart . CustomerId,
                                                                    StorefrontContext . Context . Language . Name,
                                                                    cart . CurrencyCode ) ;
        var customProperties = new Dictionary < string, string > ( ) ;
        customProperties . Add (_eoriPropertyName, shippingAddress . EORINumber ) ;

        string customPropertiesString = JsonConvert . SerializeObject (customProperties ) ;
        Proxy . DoCommand (container . SetCustomCartProperties (cart . ExternalId, customPropertiesString ) ) ;
    }
}
  • To access the custom properties set on the commerce cart through the commerce connect cart entity, we need to create our TranslateCartToEntity processor. We need to register this processor by adding it to the plugin configuration.
public class CustomTranslateCartToEntity : TranslateCartToEntity
{
    public CustomTranslateCartToEntity (IEntityFactory entityFactory ) : base (entityFactory )
    {

    }

    protected override void Translate (TranslateCartToEntityRequest request, Cart source, CommerceCart destination )
    {
        base . Translate (request, source, destination ) ;
        TranslateCustomCartProperties (source, destination ) ;
    }

    private void TranslateCustomCartProperties (Sitecore . Commerce . Plugin . Carts . Cart source, CommerceCart destination )
    {
        var customCartPropertiesComponent = source . Components . OfType <CustomCartPropertiesComponent > ( ) . FirstOrDefault ( ) ;
        if (customCartPropertiesComponent != null )
        {
            var properties = customCartPropertiesComponent . Properties ;
            foreach ( var property in properties )
            {
                destination . SetPropertyValue (property . Key, property . Value ) ;
            }
        }
    }
}