allBlogsList

Sitecore Experience Commerce - Consolidating Username and Email

How to consolidate Username and Email in Sitecore Commerce

Some eCommerce sites have separate fields for Username and Email Address while others have only one field where your email is your username. Sitecore Experience Commerce (SXC), comes with separate fields, even if the default SXA Storefront only displays Email during registration, the value entered is stored in 2 separate fields 'Username' and 'Email' in the Customer Entity. However, if you update your email afterwards, the username remain unchanged. Some customers prefer to have these 2 fields consolidated, so that when a user updates his email, the username is also updated. In this blog post i will walk you through the different steps to consolidate 'Username' and 'Email' fields in Sitecore Commerce. P.S: In this post, I assume you are familiar with extending and overriding Sitecore Commerce Engine, SXA and Sitecore Pipelines. If not, there are many Blog Posts available to get you started.

First, it is important to note where each of  Username and Email fields are stored.

Username is stored in:

  • Sitecore Core Database (Aspnet_Users table).
  • xDB Shard 0 and Shard 1 Databases (xdb_collection_ContactIdentifiers table).
  • Sitecore Commerce Customer Entity (Shared DB)

Email is stored in:

  • Sitecore Core Database (Aspnet_Memberships table).
  • xDB Shard 0 and Shard 1 Databases  (xdb_collection_ContactFacets table).
  • Sitecore Commerce Customer Entity (Shared DB).

When a user updates his email, Username remains unchanged while email is updated in the listed sources above.

User Updates are executed using 'commerce.customers.updateUser' pipeline.

This is the pipeline where we will need to override some of the functions in order to consolidate Username and Email.

Let's take a look at the processors that form this pipeline using <Your site url>/sitecore/admin/showconfig.aspx

<commerce.customers.updateUser patch:source="Sitecore.Commerce.Customers.config">
    <processor type="Sitecore.Commerce.Engine.Connect.Pipelines.Customers.UpdateUser, Sitecore.Commerce.Engine.Connect" patch:source="Sitecore.Commerce.Engine.Connectors.Customers.config">
    </processor>
    <processor type="Sitecore.Commerce.Pipelines.Customers.UpdateUser.UpdateUserInSitecore, Sitecore.Commerce.Connect.Core">
        <param ref="sitecoreUserRepository"/>
    </processor>
    <processor type="Sitecore.Commerce.Pipelines.Customers.UpdateContact.UpdateContactInXDb, Sitecore.Commerce.Connect.Core">
        <param ref="sitecoreUserRepository"/>
    </processor>
    <processor type="Sitecore.Commerce.Pipelines.Customers.Common.TriggerUserPageEvent, Sitecore.Commerce.Connect.Core">
        <Name>User Account Updated</Name>
        <Text>User account has been updated.</Text>
    </processor>
</commerce.customers.updateUser>

The first processor 'Sitecore.Commerce.Engine.Connect.Pipelines.Customers.UpdateUser' is responsible of calling Commerce Engine to update the customer entity in Commerce Shared DB.

The second processor 'Sitecore.Commerce.Pipelines.Customers.UpdateUser.UpdateUserInSitecore' is responsible of updating the user in Sitecore Core DB.

The third processor 'Sitecore.Commerce.Pipelines.Customers.UpdateContact.UpdateContactInXDb' is responsible of updating contact facets in xDB Shard 0 and Shard 1 tables.

Let's now walk through details steps:

1) Pass an additional 'OriginalUserName' parameter to the pipeline:

The UpdateUser pipeline is being trigerred from 'Sitecore.Commerce.XA.Foundation.Connect.Managers.AccountManager.UpdateUser'.

We need to override this method to pass the original username like this (Code in Bold):

    CommerceUser result = user.Result;
    if (result != null)
    {
        result.FirstName = firstName;
        result.LastName = lastName;
        result.SetPropertyValue("OriginalUserName", result.UserName);
        result.Email = emailAddress;
        result.SetPropertyValue("Phone", (object) phoneNumber);
        try
        {
          updateUserResult2 = this.CustomerServiceProvider.UpdateUser(new UpdateUserRequest(result));
        }
        catch (Exception ex)
        {
          UpdateUserResult updateUserResult3 = new UpdateUserResult();
          updateUserResult3.Success = false;
          updateUserResult2 = updateUserResult3;
          updateUserResult2.SystemMessages.Add(new SystemMessage()
          {
            Message = ex.Message + "/" + ex.StackTrace
          });
        }
    }

2**)** Update Username in Sitecore Core DB - Aspnet_Users table:

There are many wasy to achieve this.

I will suggest we override 'Sitecore.Commerce.Pipelines.Customers.UpdateUser.ProcessSitecoreUser'.

This method's implementation looks like this OOB:

    protected override void ProcessSitecoreUser(CommerceUser user, UserRequestWithUser request, ServiceProviderResult result)
    {
      this.UserRepository.Update(user);
    }

We can add some logic to update Aspnet_Users table as a first step. Simply using .Net System.Data.SqlClient to run a sql update query that would look like this:

UPDATE aspnet_Users SET UserName=@NewUsername,LoweredUserName=@LoweredNewUsername WHERE UserName=@OldUsername

3) Update Username in xDB:

Here we need to override 'Sitecore.Commerce.Pipelines.Customers.UpdateContact.UpdateContactInXDb' as follows:

    protected override void UpdateContact(UserResultWithUser result)
{
var userNameUpdated = result.CommerceUser.GetProperties().Any(p => p.Key.Equals("OriginalUserName", StringComparison.OrdinalIgnoreCase));
if (!userNameUpdated)
{
base.UpdateContact(result);
return;
}

var userIdentifier = result.CommerceUser.GetPropertyValue("OriginalUserName").ToString();
using (var client = SitecoreXConnectClientConfiguration.GetClient())
{
try
{
var contactReference = new IdentifiedContactReference("CommerceUser", userIdentifier);
var contact = client.Get(contactReference, new ContactExpandOptions("Emails", "Personal"));
if (contact == null)
{
var str = Translate.Text("Could not retrieve Contact from xDB to update for Commerce User");
result.SystemMessages.Add(new SystemMessage
{
Message = str
});
}
else
{
var identifierToRemove = contact.Identifiers.FirstOrDefault(x => x.Source == "CommerceUser");
if (identifierToRemove != null)
{
client.RemoveContactIdentifier(contact, identifierToRemove);
}
client.AddContactIdentifier(contact, new ContactIdentifier("CommerceUser", result.CommerceUser.UserName, ContactIdentifierType.Known));
this.UpdateFacets(contact, client, result);
client.Submit();
}
}
catch (Exception ex)
{
var str = Translate.Text("Could not create a Contact in xDB for the Commerce User");
result.SystemMessages.Add(new SystemMessage
{
Message = str
});
result.SystemMessages.Add(new SystemMessage
{
Message = ex.Message
});
result.Success = false;
}
}
}

5) Update Username in XC Commerce Engine:

-Override 'Sitecore.Commerce.Plugin.Customers.UpdateCustomerDetailsBlock' to remove the UserName condition (in bold) from this statement:

     if (!detailsProperty.Equals(policy?.AccountNumber, StringComparison.OrdinalIgnoreCase) &&
         !detailsProperty.Equals(policy?.LoginName, StringComparison.OrdinalIgnoreCase) &&
         (!detailsProperty.Equals(policy?.Domain, StringComparison.OrdinalIgnoreCase) &&
         !detailsProperty.Equals(policy?.UserName, StringComparison.OrdinalIgnoreCase)))
     ...

-Add a new block to 'IUpdateCustomerDetailsPipeline' to update customer index entity:

    public class UpdateAndPersistCustomerIdIndexBlock : PipelineBlock<Customer, Customer, CommercePipelineExecutionContext>
    {
        public UpdateAndPersistCustomerIdIndexBlock(IPersistEntityPipeline persistEntityPipeline,
                                              IFindEntityPipeline findEntityPipeline,
                                              IDeleteEntityPipeline deleteEntityPipeline)
        {
            this._persistPipeline = persistEntityPipeline;
            this._findEntityPipeline = findEntityPipeline;
            this._deleteEntityPipeline = deleteEntityPipeline;
        }

        public override async Task<Customer> Run(Customer arg, CommercePipelineExecutionContext context)
        {
            Condition.Requires(arg).IsNotNull($"Block {this.Name}: The customer can not be null");
            var existingCustomer = await this._findEntityPipeline.Run(new FindEntityArgument(typeof(Customer), arg.Id), context) as Customer;
            if (existingCustomer == null)
            {
                return null;
            }
            var changeUserName = !existingCustomer.UserName.Equals(arg.UserName, StringComparison.OrdinalIgnoreCase);
            if (changeUserName)
            {
                var newEntityIndex = new EntityIndex
                {
                    Id = $"{ EntityIndex.IndexPrefix<Customer>("Id")}{ arg.UserName}",
                    IndexKey = arg.UserName,
                    EntityId = arg.Id
                };
                await this._persistPipeline.Run(new PersistEntityArgument(newEntityIndex), context);
                await this._deleteEntityPipeline.Run(new DeleteEntityArgument($"{EntityIndex.IndexPrefix<Customer>("Id")}{existingCustomer.UserName}"), context);
            }

            return arg;
        }
    }

-Add a new block to 'IUpdateCustomerDetailsPipeline' to update username in customer's existing orders:


    public class UpdateCustomerUserNameInOrders : PipelineBlock<Customer, Customer, CommercePipelineExecutionContext>
    {
        private readonly CommerceCommander _commerceCommander;

        public UpdateCustomerUserNameInOrders(CommerceCommander commerceCommander)
        {
            this._commerceCommander = commerceCommander;
        }

        public override async Task<Customer> Run(Customer customer, CommercePipelineExecutionContext context)
        {
            var listName = string.Format(context.GetPolicy<KnownOrderListsPolicy>().CustomerOrders, customer.Id);
            var commerceList = await this._commerceCommander.Command<FindEntitiesInListCommand>().Process<Order>(context.CommerceContext, listName, 0, int.MaxValue);

            if (commerceList == null || !commerceList.Items.Any())
                return customer;

            foreach (var order in commerceList.Items)
            {
                var contactComponent = order.GetComponent<ContactComponent>();
                if (!string.IsNullOrEmpty(contactComponent.Email) && !contactComponent.Email.Equals(customer.Email))
                {
                    contactComponent.Email = customer.Email;
                    await this._commerceCommander.Pipeline<IPersistEntityPipeline>().Run(new PersistEntityArgument(order), context);
                }
            }

            return customer;
        }
    }

Then register the 2 blocks in ConfigureSitecore.cs:

    ConfigurePipeline<IUpdateCustomerDetailsPipeline>(configure =>
        {
            configure.Add<UpdateAndPersistCustomerIdIndexBlock>().Before<PersistCustomerBlock>();
            configure.Add<UpdateCustomerUserNameInOrders>().After<PersistCustomerBlock>();
        })

And we're all set.

Next time a user updates his Email, the Username will be updated as well.

I hope this helps if you're in the situation where you need Email and Username to be used as one field.

This solution was implemented and tested on Sitecore Experience Commerce 9.0.2.