Sitecore Commerce - Extend Create User Pipeline

Sitecore 9.2 comes with an out of the box pipeline to create users. We wanted to extend that pipeline by sending one more parameter for the purpose of receiving a newsletter and to save that info on customers. The web pipeline is invoked:

var customerService = new CustomerServiceProvider();
var request = new CreateUserRequest("JohnSmith", "password", "john@abczyx.net", "webstore");
var result = customerService.CreateUser(request);

to send additional data to pipeline:

 request.Properties.Add(new PropertyItem("JoinNewsletter", true));

In properties we can send dictionary key-value pairs, and the value is an object. We needed to send one parameter that is boolean, but it is possible to send multiple parameters or objects.

Web pipeline has different processors, like create user in Commerce, create user in Sitecore, goals etc. We wanted to extend the creation of users in Commerce. So we had to override Sitecore.Commerce.Engine.Connect.Pipelines.Customers.CreateUser in Sitecore.Commerce.Engine.Connect.dll

Extend Customer Config Changes

We created our CustomCreateUser processor that is a modification of Sitecore.Commerce.Engine.Connect.Pipelines.Customers.CreateUser code. New code is marked with comments as follows:

public class CustomCreateUser : Sitecore.Commerce.Engine.Connect.Pipelines.Customers.CreateUser
{
    public CustomCreateUser(IEntityFactory entityFactory) : base(entityFactory)
    {
    }

    public override void Process(ServicePipelineArgs args)
    {
        CreateUserRequest request;
        CreateUserResult result;
        ValidateArguments<CreateUserRequest, CreateUserResult>(args, out request, out result);
        Sitecore.Diagnostics.Assert.IsNotNull((object)request.UserName, "request.UserName");
        Sitecore.Diagnostics.Assert.IsNotNull((object)request.Password, "request.Password");
        Container container = this.GetContainer(request.Shop.Name, string.Empty, currency: args.Request.CurrencyCode);
        CommerceUser commerceUser1 = result.CommerceUser;
        if (commerceUser1 != null && commerceUser1.UserName.Equals(request.UserName, StringComparison.OrdinalIgnoreCase))
        {
            string entityId = "Entity-Customer-" + request.UserName;
            ServiceProviderResult currentResult = new ServiceProviderResult();
            EntityView entityView = this.GetEntityView(container, entityId, string.Empty, "Details", string.Empty, currentResult);
            if (currentResult.Success && !string.IsNullOrEmpty(entityView.EntityId))
            {
                base.Process(args);
                return;
            }
        }
        EntityView entityView1 = this.GetEntityView(container, string.Empty, string.Empty, "Details", "AddCustomer", (ServiceProviderResult)result);
        if (!result.Success)
            return;
        entityView1.Properties.FirstOrDefault<ViewProperty>((Func<ViewProperty, bool>)(p => p.Name.Equals("Domain", StringComparison.OrdinalIgnoreCase))).Value = request.UserName.Split('\\')[0];
        entityView1.Properties.FirstOrDefault<ViewProperty>((Func<ViewProperty, bool>)(p => p.Name.Equals("LoginName", StringComparison.OrdinalIgnoreCase))).Value = request.UserName.Split('\\')[1];
        entityView1.Properties.FirstOrDefault<ViewProperty>((Func<ViewProperty, bool>)(p => p.Name.Equals("AccountStatus", StringComparison.OrdinalIgnoreCase))).Value = "ActiveAccount";
        if (!string.IsNullOrEmpty(request.Email))
            entityView1.Properties.FirstOrDefault<ViewProperty>((Func<ViewProperty, bool>)(p => p.Name.Equals("Email", StringComparison.OrdinalIgnoreCase))).Value = request.Email;
        //new custom code for getting data to commerce pipeline
        entityView1.Properties.Add(new ViewProperty
        {
            Name = "JoinNewsletter",
            Value = request.Properties["JoinNewsletter"].ToString(),
            UiType = "string",
            IsHidden = true,
            IsReadOnly = true
        });
        //end of a new code
        CommerceCommand commerceCommand = this.DoAction(container, entityView1, (ServiceProviderResult)result);
        if (commerceCommand != null && commerceCommand.ResponseCode.Equals("ok", StringComparison.OrdinalIgnoreCase))
        {
            //modification here, we couldn't use entityFactory
            CommerceUser commerceUser2 = new CommerceUser();
            commerceUser2.Email = request.Email;
            commerceUser2.UserName = request.UserName;
            commerceUser2.ExternalId = commerceCommand.Models.OfType<CustomerAdded>().FirstOrDefault<CustomerAdded>().CustomerId;
            result.CommerceUser = commerceUser2;
            request.Properties.Add(new PropertyItem()
            {
                Key = "UserId",
                Value = (object)result.CommerceUser.ExternalId
            });
        }
        base.Process(args);
    }
    // had to copy method from dll because there is internal
    private  static void ValidateArguments<TRequest, TResult>(
        ServicePipelineArgs args,
        out TRequest request,
        out TResult result)
        where TRequest : ServiceProviderRequest
        where TResult : ServiceProviderResult
    {
        Sitecore.Diagnostics.Assert.ArgumentNotNull((object)args, nameof(args));
        Sitecore.Diagnostics.Assert.ArgumentNotNull((object)args.Request, "args.Request");
        Sitecore.Diagnostics.Assert.ArgumentNotNull((object)args.Request.RequestContext, "args.Request.RequestContext");
        Sitecore.Diagnostics.Assert.ArgumentNotNull((object)args.Result, "args.Result");
        request = args.Request as TRequest;
        result = args.Result as TResult;
        Sitecore.Diagnostics.Assert.IsNotNull((object)request, "The parameter args.Request was not of the expected type.  Expected {0}.  Actual {1}.", typeof(TRequest).Name, args.Request.GetType().Name);
        Sitecore.Diagnostics.Assert.IsNotNull((object)result, "The parameter args.Result was not of the expected type.  Expected {0}.  Actual {1}.", typeof(TResult).Name, args.Result.GetType().Name);
    }
}

This processor is patched in Project/NameOfAProject/code/App_Config/Include/Y.Commerce.Engine.Conectors.Customers.config:

<commerce.customers.createUser>
        <processor type="Sitecore.Commerce.Pipelines.Customers.CreateUser.CreateUserInExternalSystem, Sitecore.Commerce.Connect.Core">
          <patch:delete/>
        </processor>
        //deleted code
        //<processor patch:before="processor[@type='Sitecore.Commerce.Pipelines.Customers.CreateUser.CreateUserInSitecore, Sitecore.Commerce.Connect.Core']" type="Sitecore.Commerce.Engine.Connect.Pipelines.Customers.CreateUser, Sitecore.Commerce.Engine.Connect">
        //added code
        <processor patch:before="processor[@type='Sitecore.Commerce.Pipelines.Customers.CreateUser.CreateUserInSitecore, Sitecore.Commerce.Connect.Core']" 
        type="NameOfAProject.Foundation.Commerce.Pipelines.Customer.CustomCreateUser, NameOfAProject.Foundation.Commerce">
          <param ref="entityFactory" />
        </processor>
      </commerce.customers.createUser>

The processor calls commerce block DoActionAddCustomerBlock so we had to override that block to extract data we are sending. ConfigureSitecore.cs:

.ConfigurePipeline<IDoActionPipeline>(configure
                {
                    configure.Replace<DoActionAddCustomerBlock, CustomDoActionAddCustomerBlock>();
                })

DoActionAddCustomerBlock is modified to CustomDoActionAddCustomerBlock by saving data in component and placing component in commerce context:

public class CustomDoActionAddCustomerBlock: DoActionAddCustomerBlock
    {
        private readonly ITranslateEntityViewToCustomerPipeline _translatePipeline;
        private readonly CreateCustomerCommand _createCustomerCommand;

        public CustomDoActionAddCustomerBlock(CreateCustomerCommand createCustomerCommand,
            ITranslateEntityViewToCustomerPipeline translatePipeline) : base(createCustomerCommand, translatePipeline)
        {
            _createCustomerCommand = createCustomerCommand;
            _translatePipeline = translatePipeline;
        }

        public override async Task<EntityView> Run(
      EntityView arg,
      CommercePipelineExecutionContext context)
        {
            if (string.IsNullOrEmpty(arg?.Action) || !arg.Action.Equals(context.GetPolicy<KnownCustomerActionsPolicy>().AddCustomer, StringComparison.OrdinalIgnoreCase))
                return arg;
            CustomerPropertiesPolicy propertiesPolicy = context.GetPolicy<CustomerPropertiesPolicy>();
            ViewProperty viewProperty1 = arg.Properties.FirstOrDefault<ViewProperty>((Func<ViewProperty, bool>)(p => p.Name.Equals(propertiesPolicy?.Domain, StringComparison.OrdinalIgnoreCase)));
            if (string.IsNullOrEmpty(viewProperty1?.Value) || !propertiesPolicy.Domains.Contains(viewProperty1?.Value))
            {
                string str1 = viewProperty1 == null ? propertiesPolicy?.Domain : viewProperty1.DisplayName;
                string str2 = await context.CommerceContext.AddMessage(context.GetPolicy<KnownResultCodes>().ValidationError, "InvalidOrMissingPropertyValue", new object[1]
                {
          (object) str1
                }, "Invalid or missing value for property '" + propertiesPolicy?.Domain + "'.").ConfigureAwait(false);
                return arg;
            }
            ViewProperty viewProperty2 = arg.Properties.FirstOrDefault<ViewProperty>((Func<ViewProperty, bool>)(p => p.Name.Equals(propertiesPolicy?.LoginName, StringComparison.OrdinalIgnoreCase)));
            if (string.IsNullOrEmpty(viewProperty2?.Value))
            {
                string str3 = viewProperty2 == null ? propertiesPolicy?.LoginName : viewProperty2.DisplayName;
                string str4 = await context.CommerceContext.AddMessage(context.GetPolicy<KnownResultCodes>().ValidationError, "InvalidOrMissingPropertyValue", new object[1]
                {
          (object) str3
                }, "Invalid or missing value for property '" + propertiesPolicy?.LoginName + "'.").ConfigureAwait(false);
                return arg;
            }

            //mew code
            var joinNewsletterComponent = new JoinNewsletterComponent
            {
                JoinNewsletter = bool.Parse(arg.Properties
                    .FirstOrDefault(p => p.Name.Equals("JoinNewsletter", StringComparison.OrdinalIgnoreCase))?.Value)
            };
            context.CommerceContext.AddObject(joinNewsletterComponent);
            //end of new code
            Customer customer1 = await this._translatePipeline.Run(arg, context).ConfigureAwait(false);
            Customer customer2 = await this._createCustomerCommand.Process(context.CommerceContext, customer1).ConfigureAwait(false);
            return arg;
        }
    }

Component that is storing data:

 public class JoinNewsletterComponent : Component
   {
       public JoinNewsletterComponent()
       {
           Name = "JoinNewsletterComponent";
       }
       public bool JoinNewsletter { get; set; } 

CustomDoActionAddCustomerBlock invokes ICreateCustomerPipeline, and in that pipeline we are storing information on a customer by adding AddJoinNewsletterBlock. ConfigureSitecore.cs:

.ConfigurePipeline<ICreateCustomerPipeline>(c =>
               {
                   c.Add<AddJoinNewsletterBlock>().After<CreateCustomerBlock>();
               })

AddJoinNewsletterBlock.cs:

public class AddJoinNewsletterBlock : PipelineBlock<Customer, Customer, CommercePipelineExecutionContext>
  {
      public override Task<Customer> Run(Customer customer, CommercePipelineExecutionContext context)
      {
          Condition.Requires(customer).IsNotNull("The customer can not be null");
          var joinNewsLetter = false;
          var joinNewsletterComponent= context.CommerceContext.GetObject<JoinNewsletterComponent>();
          if (joinNewsletterComponent != null) joinNewsLetter = joinNewsletterComponent.JoinNewsletter;
          customer.GetComponent<JoinNewsletterComponent>().JoinNewsletter = joinNewsLetter;
          return Task.FromResult(customer);
      }
  }

This has been implemented and tested on Sitecore Experience Commerce 9.2

Thank You for Reading
Share This Page