Transitioning from Sitecore Experience Commerce to OrderCloud: Customers to Buyer Users

In this article, we will review and compare Sitecore Experience Commerce customers and OrderCloud buyer users to facilitate developers looking to transition from XC to OrderCloud as well as identify a path for migration of existing XC solutions to OrderCloud.

We will look at a high-level comparison of architecture, functionality, and data models, with greater focus on transitioning from Sitecore Experience Commerce, meaning identifying a close path to parity in OrderCloud, using the Habitat catalog and SXA Storefront.

Customers

The Customer to Storefront Association

In an XC SXA implementation, customer accounts are registered under a security domain, which can be configured in the Content Editor on the /sitecore/Content/<tenant>/<site>/Settings/Site Grouping/<storefront> item.

Figure 1: An example domain configuration for a storefront.

Customers can only be registered to a single security domain and will not be accessible in storefronts configured to a different domain. A separate customer account would need to be created under the other security domains, but it would have no knowledge or relationship to accounts across domains. However, a security domain can be registered to multiple storefronts allowing a customer to share an account across storefronts.

Figure 2: XC Security Domains have a zero to many relationship with Storefronts.

In OrderCloud, customers can be represented as buyer users, which are registered under buyer companies, and can only belong to a single buyer company. This can be compared directly to the relationship between security domains and customers.

The relationship of a buyer to an OrderCloud storefront is not tightly coupled like the SXA storefront. Access to an OrderCloud storefront is managed through API clients, security profiles and their assignments to buyers. See Getting Started > Establishing API Access for more information.

Figure 3: The basic structure comparison between customers to users.

The Customer to Catalog Association

Buyers also contain a DefaultCatalogID property, which will create an associated catalog with an ID matching the buyer ID if not specified during the creation of a buyer. While it may seem like this limits Buyer A to a single catalog (Catalog A), multiple catalogs can be assigned via OrderCloud's catalog assignments, which matches XC's equivalent in supporting both Catalog A and Catalog C associations to Security Domain A via storefronts (figure 4).

The DefaultCatalogID property will be utilised in certain APIs when a catalogID is not supplied in their request, however is can be overridden. For now it is only important to understand that this is not a limiting factor in OrderCloud functionality for migration of data or functionality.

Figure 4: XC relationship between customers and catalogs vs. OrderCloud's relationship between buyer users and catalogs.

OrderCloud User Groups

OrderCloud also allows buyer users to be assigned to one or more buyer user groups, which can be used to supersede assignments to buyer users in bulk, e.g. provide access to special pricing for VIP buyer users.

Figure 5: OrderCloud can group buyer users in buyer user groups.

Over in XC, a common customisation is an implementation of customer groups to assign customers in order to provide customer-group specific pricing, promotions, etc. While the implementation details may have the customer group represented as a separate custom entity or a property on a customer, conceptually both approaches achieve the same outcome (figure 6) and are on par with OrderCloud's architecture (figure 5), allowing a seamless transition path to OrderCloud.

Figure 6: Customer groups are a common customisation in XC, which can be thought of in a simliar respect to OrderCloud's buyer user groups, however it is not part of the default ecosystem.

Customer Addresses

In XC, customers have explicit addresses associations as the address component lives on the customer entity. The IsPrimary flag is used to allow a default address selection in implementations.

Figure 7: Customer addresses in XC.

In OrderCloud, addresses are separate objects that have an immediate association to the buyer, then with buyer address assignments addresses can have direct assignments to the buyer users or indirect assignments via buyer user groups.

Figure 8: Buyer address architecture in OrderCloud.

To bring across customer addresses into OrderCloud figure 9 shows that we would simply create buyer addresses with buyer address assignments for the buyer user only, not the buyer user groups, as well as adding the IsPrimary flag to the buyer address xp.

Figure 9: OrderCloud's representation of XC customer structure.

For buyer address assignments, there are two additional properties to consider, in IsShipping and IsBilling. These properties control whether the address can be assigned to an order as a shipping address or billing address respectively. As the SXA Storefront allows customer addresses to be used for both of these addresses, these flags on the buyer address assignments should be set to true.

Data Mapping

With the conceptual analysis above, we will now review what data mapping would look like for migration and from a comparison standpoint.

In the XC Entity/Component column, components are assumed to live on the primary XC entity being mapped.

OrderCloud IDs do not allow spaces. It is important that the IDs are parsed to remove/replace invalid characters consistently.

Buyers

The DefaultCatalogID property will be reviewed further during the catalog analysis article.

OC PropertyData TypeRequiredXC Entity/ComponentXC PropertyData TypeNotes
IDstringNoCustomerDomainstring
ActivestringYesN/AN/AN/ASet to true by default.
NamestringNoCustomerDomainstring
DefaultCatalogIDstringYesN/AN/AN/AAssign to an existing catalog or PATCH later.
xpobjectNoN/AN/AN/AAny custom fields can be mapped to xp.

Buyer Users

There are a few properties in OrderCloud are required while not mandatory in the SXA Storefront, so considerations for handling these properties will need to be made in these instances, e.g. applying fallback values.

OC PropertyData TypeRequiredXC Entity/ComponentXC PropertyData TypeNotes
buyerIDstringYesCustomerDomainstringIs a resource parameter, not body property.
IDstringNoCustomerFriendlyIdstring
UsernamestringYesCustomerLoginNamestring
PasswordstringNoN/AN/AN/APassword should not be migrated.
FirstNamestringYesCustomerFirstNamestringNot a mandatory field in SXA, so a fallback value will need to be populated.
LastNamestringYesCustomerLastNamestringNot a mandatory field in SXA, so a fallback value will need to be populated.
EmailstringYesCustomerEmailstring
PhonestringNoCustomerDetailsComponentPhoneNumberstringThe XC PhoneNumber exists under an entity view of the CustomerDetailsComponent, so consider this a rough mapping.
TermsAcceptedstringNoN/AN/AN/A
ActivebooleanYesCustomerAccountStatusstringSet to true where AccountStatus == "ActiveAccount"
xpobjectNoN/AN/AN/AAny custom fields can be mapped to xp.

Buyer Addresses

For buyer addresses, IDs need to be unique, therefore creating a convention to append the customer's FriendlyId and the XC address' unique identifier, AddressName, ensures there will be no conflicts.

OrderCloud only stores the ISO 3166-1 alpha-2 2-letter country code. Retrieving additional country details, such as country name, would best be handled by custom middleware, outside of OrderCloud.

OC PropertyData TypeRequiredXC Entity/ComponentXC PropertyData TypeNotes
buyerIDstringYesCustomerDomainstringIs a resource parameter, not body property.
IDstringNoCustomer
AddressComponent
FriendlyId
Party.AddressName
stringUsing the convention <Customer.FriendlyId>_<Party.AddressName> will keep the address ID unique.
FirstNamestringNoAddressComponentFirstNamestringSXA Storefront does not capture FirstName. May wish to use Customer.FirstName instead.
LastNamestringYesAddressComponentLastNamestringSXA Storefront does not capture LastName. May wish to use Customer.LastName instead.
Street1stringYesAddressComponentParty.Address1string
Street2stringNoAddressComponentParty.Address2string
CitystringYesAddressComponentParty.Citystring
StatestringYesAddressComponentParty.StateCodestringCan also use Party.State for full state name, depending on requirement.
ZipstringYesAddressComponentParty.ZipPostalCodestring
CountrystringYesAddressComponentParty.CountryCodestring
PhonestringNoAddressComponentParty.PhoneNumberstringWill be empty in SXA Storefront default implementation. May wish to use Customer.[CustomerDetailsComponent].PhoneNumber instead.
AddressNamestringNoAddressComponentParty.AddressNamestring
xpobjectNoN/AN/AN/AAny custom fields can be mapped to xp.
xp.IsPrimaryobjectNoAddressComponentParty.IsPrimaryboolean

Buyer Address Assignments

Buyer address assignments are a straight forward mapping as per the table below.

OC PropertyData TypeRequiredXC Entity/ComponentXC PropertyData TypeNotes
buyerIDstringYesCustomerDomainstringIs a resource parameter, not body property.
AddressIDstringYesCustomer
AddressComponent
FriendlyId
Party.AddressName
string<Customer.FriendlyId>_<Party.AddressName> to keep the address ID unique
UserIDstringNoCustomerFriendlyIdstring
UserGroupIDstringNoN/AN/AN/A
IsShippingstringNoN/AN/AN/ASet to true.
IsBillingstringNoN/AN/AN/ASet to true.

References

Configuring Customers for Multi-Commerce-Site Solutions

Overview

In this post I will be taking you through the configuration and implementation required to enable separate customer accounts per site in a multi-commerce-site solution.

Note: We won't go through the process of adding a second commerce storefront in this post.

Understanding Customer Accounts

Let us first understand how customer accounts are associated to a site using the Sitecore Retail Demo as our reference solution.

First of all, we review the Sitecore configurations, via http://retail.dev.local/sitecore/admin/showconfig.aspx, for pipelines that create users.  We see that the commerce.customers.createUser pipeline employs the SitecoreUserRespository, which handles all CRUD operations for users. Notably, the domain parameter of the repository, 'CommerceUsers' , is utilised to construct the user's name in the format <domain>\<website user name>.

This is great news!

We have just identified that customer accounts are registered with a domain, so our next step is to identify how to configure the domain per site, so that users don't share the same account and login details across sites in a multi-site solution.

Now, we could update the domain on the repository configuration, but this only solves the issue of updating the domain in a single domain implementation. We need to investigate the 'CommerceUsers' domain further.

Further Investigation and Implementation

Looking back into the Sitecore configurations, we want to identify other usages instances of 'CommerceUsers' to determine if they will be relevant to our customisation. We can see that this domain is associated to the following:-

  1. Profile import
    <sitecore>
      <commerceServer>
        <ProfileImport patch:source="CommerceServer.Profiles.config">
          <setting name="FieldForUserNameGeneration" value="GeneralInfo.email_address"/>
          <setting name="EmailAddressFieldName" value="GeneralInfo.email_address"/>
          <setting name="ProfileUniqueQueryKey" value="GeneralInfo.user_id"/>
          <setting name="ErrorThreshold" value="10"/>
          <setting name="Domain" value="CommerceUsers"/>
          <setting name="OutFile" value=""/>
          <setting name="MaxDegreeOfParallelism" value="4"/>
        </ProfileImport>
      </commerceServer>
    </sitecore>
    
  2. Site
    <sitecore>
      <sites>
        <site name="storefront" targetHostName="retail.dev.local" hostName="retail|storefront" commerceShopName="storefront" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content/storefront" startItem="/Home" dictionaryPath="/sitecore/content/storefront/global/dictionary" dictionaryAutoCreate="false" placeholderSettingsRoot="/sitecore/layout/Placeholder Settings/Project/Retail" mailTemplatesRoot="/sitecore/content/Storefront/Global/Mails" domain="CommerceUsers" allowDebug="true" htmlCacheSize="50MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="25MB" filteredItemsCacheSize="10MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" cacheRenderingParameters="true" renderingParametersCacheSize="10MB" formsRoot="/sitecore/system/Modules/Web Forms for Marketers/Retail" loginPage="/login" enableItemLanguageFallback="true" patch:source="z.Sitecore.Demo.Retail.DevSettings.config" database="master" cacheHtml="false"/>
      </sites>
    </sitecore>
    
  3. Sitecore user repository
    <sitecore>
      <sitecoreUserRepository type="Sitecore.Commerce.Data.Customers.SitecoreUserRepository, Sitecore.Commerce" singleInstance="true" patch:source="Sitecore.Commerce.Customers.config">
        <param ref="entityFactory"/>
        <param desc="serializationProperty">__Serialization</param>
        <param desc="domain">CommerceUsers</param>
        <param desc="role">User Role</param>
      </sitecoreUserRepository>
    </sitecore>
    
  4. Profile provider for the Commerce Server
    <sitecore>
      <switchingProviders>
        <profile>
          <provider providerName="cs" storeFullNames="true" wildcard="%" domains="CommerceUsers" patch:source="CommerceServer.Profiles.config"/>
        </profile>
      </switchingProviders>
    </sitecore>
    

Profile Import

The profile import can be identified as out of scope for the base minimum requirements of setting up separate customer accounts per site, so we will leave this as an exercise to be completed separately leveraging the knowledge obtained from this investigation and implementation.

Site

Logically, this is the starting point for unique site configurations, but without the initial investigation developers would most likely fall into the trap of assuming the site's domain attribute was the source domain used in creating users.

As we know that we should utilise the site configured domain when creating users, we don't have to change the value of the domain for now. We will need to keep in mind that SitecoreUserRepository should be retrieving the value from the site configuration and not the static parameter it currently utilises.

Sitecore User Repository

In our initial investigation we determined that this repository was using the domain parameter to construct the db user's name , however we didn't identify how we could replace the parameter with the site domain attribute.

Taking a look closer at how the domain parameter is utilised within the repository, we see that there is a virtual method GetDomain() which we can override to return the site's domain. Let's do this now.

The Sitecore.Foundation.Multisite project seems to be the appropriate place to house the implementation to override the GetDomain() method as the project itself and the customisation we are about to implement are both agnostic of site-specific details.

The following snippet of codes shows how trivial updating the domain is by simply returning the site's domain name. (You will need to reference the Sitecore.Commerce.dll if the project does not already include it)

using Sitecore.Commerce.Entities;

namespace Sitecore.Foundation.MultiSite.Infrastructure.Pipelines
{
  public class SitecoreUserRepository : Sitecore.Commerce.Data.Customers.SitecoreUserRepository
  {
    public SitecoreUserRepository(IEntityFactory entityFactory, string serializationProperty, string domain, string role)
      : base(entityFactory, serializationProperty, domain, role)
    {
    }

    protected override string GetDomain()
    {
      return Context.Domain.Name;
    }
  }
}

We follow this up by registering class in the config

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <sitecoreUserRepository type="Sitecore.Commerce.Data.Customers.SitecoreUserRepository, Sitecore.Commerce">
      <patch:attribute name="type">Sitecore.Foundation.MultiSite.Infrastructure.Pipelines.SitecoreUserRepository, Sitecore.Foundation.MultiSite</patch:attribute>
    </sitecoreUserRepository>
  </sitecore>
</configuration>

We publish our changes, check the showconfig page to ensure the deploy was successful, and register a user (andrew2@test.com) with the debugger attached at a break point with our custom code as a sanity check that the code is being executed. Success!

If we change the domain in our site config to 'DomainA' we should expect to see a user registered under the new domain, right? Let's check.

Register: The external id for the user default\andrew3@test.com is empty.

What happened? Well, upon investigation there are 2 issues here. The first is that the user was created in our Sitecore core database dbo.aspnet_Users table, however the user was not created as a commerce user in the Commerce Server profiles database dbo.UserObject table. The second is that the domain name does not match the 'DomainA' we applied and instead has been set to 'default'.

As we still need to look at the configuration for Profile provider for the Commerce Server, let's start there.

Profile provider for the Commerce Server

Following on from missing user in the Commerce Server profile database and our earlier investigation, we determined that the Commerce Server configuration has not been updated to accept profiles from a domain other than 'CommerceUsers', so let's update the attribute to 'DomainA' and see what happens.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <switchingProviders>
      <profile>
        <provider providerName="cs">
          <patch:attribute name="domains">DomainA</patch:attribute>
        </provider>
      </profile>
    </switchingProviders>
  </sitecore>
</configuration>

Similar to the SitecoreUserRepository configuration, we have just updated the domains attribute to handle a single domain solution, however the domains attribute can be configured with a comma-separated list of values, so no additional customisation is required other than to set the additional domains as required. i.e.

<patch:attribute name="domains">DomainA,DomainB</patch:attribute>

Configuring Domains

We appear to have missed something. In \Website\App_Config\Security folder, the Domains.config contains the registration for domains. In this instance we can just update 'CommerceUsers' directly to 'DomainA' in the domains configuration, or copy the domain definition and change the domain name appropriately. We add the file to our solution, publish and test again.

Note: Domains can also be maintained in the Domain Manager, but by adding the file to our solution, we can ensure that all developers domain configurations are automated and kept in sync via solution deployments.

<?xml version="1.0" encoding="utf-8"?>
<domains xmlns:sc="Sitecore">
  <domain name="sitecore" ensureAnonymousUser="false" />
  <domain name="extranet" />
  <domain name="default" isDefault="true" />
  <sc:templates>
    <domain type="Sitecore.Security.Domains.Domain, Sitecore.Kernel">
      <ensureAnonymousUser>true</ensureAnonymousUser>
      <locallyManaged>false</locallyManaged>
    </domain>
  </sc:templates>
  <domain name="DomainA" defaultProfileItemId="{0FFEC1BD-4D35-49CF-8B7D-7F01930834ED}" ensureAnonymousUser="false" />
  <domain name="CommerceCustomers" />
  <domain name="modules"/>
  <domain name="habitat"/>
  <domain name="DomainB" defaultProfileItemId="{0FFEC1BD-4D35-49CF-8B7D-7F01930834ED}" ensureAnonymousUser="false" />
</domains>

Success!

We are able to register users under a different domain that is associated with the site via configuration of the domain attribute. The accounts have also been confirmed in the Sitecore core database dbo.aspnet_Users table, and the Commerce Server profiles database dbo.UserObject table.

In the future, if we want to change the domain we only need update the site's domain attribute and ensure the domain is registered in the Domains.config.

Summary

In wrapping up, we managed to get a better understanding of how customer accounts are associated to a site (by domain, not directly tied to a site), we were able to identify and implement the customisations required in order to support a multi-commerce-site solution, and we also ended up getting our solution into a state we can manage adding and assigning domains via config to be deployed with the solution without any manual steps outside of configuration.