Skip to content

Commit ae6d1e3

Browse files
committed
Introduce Account-level matching & mapping to allow supporting Account-level fields on PersonAccounts
1 parent 788a4e2 commit ae6d1e3

File tree

56 files changed

+1682
-397
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1682
-397
lines changed

README.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Salesforce B2C Commerce / CRM Sync is an enablement solution designed by Salesfo
1111

1212
b2c-crm-sync includes a framework for integrating these clouds (ex. B2C Commerce and Service Cloud) -- leveraging REST APIs and the declarative capabilities of the Salesforce Platform. This approach powers frictionless customer experiences across B2C Commerce, Service, and Marketing Clouds by resolving and synchronizing customer profiles across these Salesforce products.
1313

14-
> :100:  This repository is currently in it's **v3.0.0** release. The MVP feature-set is complete, and you can now deploy b2c-crm-sync to scratchOrgs and sandboxes via its CLI tooling. Solution trustworthiness is critical for our success. Please use the tagged release, but also feel free to deploy from master if you want to work with the latest updates.  :100:
14+
> :100:  This repository is currently in it's **v3.0.0** release. You can now deploy b2c-crm-sync to scratchOrgs and sandboxes via its CLI tooling. Solution trustworthiness is critical for our success. Please use the tagged release, but also feel free to deploy from master if you want to work with the latest updates.  :100:
1515
1616
Please visit our [issues-list](https://github.com/SalesforceCommerceCloud/b2c-crm-sync/issues) to see outstanding issues and features, and visit our [discussions](https://github.com/SalesforceCommerceCloud/b2c-crm-sync/discussions) to ask questions.
1717

@@ -24,6 +24,12 @@ b2c-crm-sync enables the resolution, synchronization, viewing, and management of
2424
2525
b2c-crm-sync leverages Salesforce B2C Commerce Open Commerce REST APIs to interact with B2C Customer Profiles -- and a Salesforce Platform REST API to 'announce' when shoppers register or modify B2C Commerce Customer Profiles. Through these announcements, the Salesforce Platform requests the identified data objects (ex. customers) via REST APIs -- and then ingests elements of those data objects to create Account / Contact or PersonAccount representations of B2C Commerce Customer Profiles.
2626

27+
Please find hereafter diagrams that shows how b2c-crm-sync is synching customer profile profiles in a bidirectional way:
28+
29+
![B2C To Core Diagram](docs/imgs/B2CtoCore.png "B2C To Core Diagram")
30+
31+
![Core To B2C Diagram](docs/imgs/CoretoB2C.png "Core To B2C Diagram")
32+
2733
### License
2834
This project, its source code, and sample assets are all licensed under the [BSD 3-Clause](License.md) License.
2935

@@ -56,6 +62,20 @@ b2c-crm-sync supports the following extensible features (yes, you can customize
5662

5763
> We leverage [Salesforce SFDX for Deployment](https://trailhead.salesforce.com/content/learn/modules/sfdx_app_dev), [Flow for Automation](https://trailhead.salesforce.com/en/content/learn/modules/flow-builder), [Platform Events for Messaging](https://trailhead.salesforce.com/en/content/learn/modules/platform_events_basics), [Salesforce Connect for Data Federation](https://trailhead.salesforce.com/en/content/learn/projects/quickstart-lightning-connect), and [Apex Invocable Actions](https://trailhead.salesforce.com/en/content/learn/projects/quick-start-explore-the-automation-comps-sample-app) to support these features. If you're a B2C Commerce Architect interested in learning how to integrate with the Salesforce Platform -- this is the project for you :)
5864
65+
### Account-level attribute matching & mapping
66+
67+
Starting since v4.0.0, we introduced a new level of data matching and data mapping between B2C Commerce and the Salesforce Core Platform.
68+
As some business requirements make fields declared at the Account level within the Core platform, and some other fields declared at the Contact level, we introduced a new level of data mapping that allows you to map fields from B2C Commerce to either the Account or the Contact level within the Core Platform.
69+
Of course, this new feature makes way more sense when you are using PersonAccounts within the Salesforce Core Platform, as both the Account & Contact records are merged under the hood.
70+
71+
As a schema is always easier to understand than a thousand words, please have a look at the following schema to understand how the B2C Commerce Customer Profile data is matched into the Core Platform:
72+
73+
![B2C To Core - Account Level Mapping Diagram](docs/imgs/B2CtoCore-AccountLevelMapping.png "B2C To Core - Account Level Mapping Diagram")
74+
75+
In order to configure account-level mapping attributes, please create a new `B2C_Integration_Field_Mappings` custom metadata with the value `Account` into the `Service_Cloud_Object__c` field.
76+
77+
> Please note that due to this new feature: the Contact duplicate rule is not not used anymore if you enable the `PersonAccount` model. It is only used by the unit tests, and thus is still required to be enabled. This happens because, if you are enabling the `PersonAccount` and have at least one account-level mapping, then b2c-crm-sync is using a person account record instead of a contact to resolve the customer profile and map data to it.
78+
5979
## Setup Guidance
6080

6181
### Deployment Considerations
@@ -969,6 +989,12 @@ npm run crm-sync:sf:connectedapps
969989
```
970990
This command creates a connectedApp for each of the B2C Commerce storefronts configured in your .env file. The B2C Commerce service definitions used to connect with your Salesforce Org use these connectedApps to connect securely.
971991

992+
> b2c-crm-sync use Username-password flows, which are blocked by default in orgs created in Summer ‘23 or later. make sure to activate it if it's not activated already
993+
- Go to settings
994+
- in the search, look for OAuth
995+
- select OAuth and OpenID Connect Settings under identity
996+
- Turn on `Allow OAuth Username-Password Flows`
997+
972998
#### Create and Deploy Your Duplicate Rules
973999
16. Duplicate rules can be configured and deployed via a CLI command that retrieves the duplicateRules configuration in the Salesforce Org, identifies which b2c-crm-sync rules already exist, and creates the rule templates to deploy. Please execute this CLI command to create and deploy duplicateRules:
9741000

@@ -1440,7 +1466,7 @@ As the B2C Commerce customer profile and its addresses are fetched by the core p
14401466
3. In the Quick Find box at the top left, search for `Custom Metadata` and click on this menu
14411467
4. Click on the `Manage records` on the row `B2C Integration Field Mapping`
14421468
5. You'll find all the data mapping here, that you can modify, remove, or add new attributes as part of the data mapping.
1443-
1469+
6. Please note that the data mapping provided here is based on standard fields. It is up to you to add/remove/update mappings based on the business requirements.
14441470

14451471
| Label | Core Object | Core ID | Core Alt ID | B2C Object | OCAPI ID |
14461472
|:-------------------:|:----------------------:|:--------------------------:|:---------------------------:|:---------------:|:-------------------:|
140 KB
Loading

docs/imgs/B2CtoCore.png

124 KB
Loading

docs/imgs/CoretoB2C.png

101 KB
Loading

src/sfcc/cartridges/int_b2ccrmsync/cartridge/scripts/b2ccrmsync/models/customer.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ Customer.prototype = {
6868
getRequestBody: function (profileDetails) {
6969
return JSON.stringify({
7070
inputs: [{
71-
sourceContact: profileDetails || this.profileRequestObjectRepresentation
71+
sourceInput: {
72+
jsonRepresentation: JSON.stringify(profileDetails || this.profileRequestObjectRepresentation)
73+
}
7274
}]
7375
});
7476
},

src/sfdc/base/main/default/classes/B2CBaseAttributeAssignment.cls

+37-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public abstract with sharing class B2CBaseAttributeAssignment {
123123
if (sourceObject.isSet(objectFieldName) && sourceObject.get(objectFieldName) != null) {
124124

125125
// If so, then evaluate if the targetObject is missing this field -- or it has it, and the value is null
126-
if (!targetObjectFields.contains(objectFieldName) || (targetObjectFields.contains(objectFieldName) && targetObject.get(objectFieldName) == null)) {
126+
if (doesFieldExist(getSchemaMap(targetObject), objectFieldName) && (!targetObjectFields.contains(objectFieldName) || (targetObjectFields.contains(objectFieldName) && targetObject.get(objectFieldName) == null))) {
127127

128128
// If the field exists, see if it has been set in the target object
129129
if (targetObject.get(objectFieldName) == null) {
@@ -205,6 +205,42 @@ public abstract with sharing class B2CBaseAttributeAssignment {
205205

206206
}
207207

208+
/**
209+
* @description This method translates a Contact into a Person Account. It does this by leveraging the
210+
* field mappings for the contact and account objects.
211+
*
212+
* @param contact {SObject} Represents the contact to translate
213+
* @param contactFieldMappings {List<B2C_Integration_Field_Mappings__mdt>} Represents the collection of field mappings to leverage for the contact
214+
* @param accountFieldMappings {List<B2C_Integration_Field_Mappings__mdt>} Represents the collection of field mappings to leverage for the account
215+
* @return {Account} Returns the translated contact into a Person Account
216+
*/
217+
public static Account translateContactToPersonAccount(Contact contact, List<B2C_Integration_Field_Mappings__mdt> contactFieldMappings, List<B2C_Integration_Field_Mappings__mdt> accountFieldMappings) {
218+
// Person Account Record Type
219+
RecordType rt = [SELECT Id, DeveloperName FROM RecordType WHERE DeveloperName = :B2CConfigurationManager.getPersonAccountRecordTypeDeveloperName() WITH SECURITY_ENFORCED];
220+
Account a = new Account(
221+
RecordTypeId = rt.Id
222+
);
223+
Map<String, Object> populatedFields = contact.getPopulatedFieldsAsMap();
224+
225+
// Loop over the contact fieldMappings and evaluate each field
226+
for (B2C_Integration_Field_Mappings__mdt thisFieldMapping : contactFieldMappings) {
227+
// Compare the attribute values for the original and processed objects
228+
if (doesFieldExist(getSchemaMap(a), thisFieldMapping.Service_Cloud_Attribute_Alt__c) && doesFieldExist(getSchemaMap(contact), thisFieldMapping.Service_Cloud_Attribute__c) && populatedFields.containsKey(thisFieldMapping.Service_Cloud_Attribute__c)) {
229+
a.put(thisFieldMapping.Service_Cloud_Attribute_Alt__c, contact.get(thisFieldMapping.Service_Cloud_Attribute__c));
230+
}
231+
}
232+
233+
// Loop over the account fieldMappings and evaluate each field
234+
for (B2C_Integration_Field_Mappings__mdt thisFieldMapping : accountFieldMappings) {
235+
// Compare the attribute values for the original and processed objects
236+
if (doesFieldExist(getSchemaMap(a), thisFieldMapping.Service_Cloud_Attribute__c) && doesFieldExist(getSchemaMap(contact), thisFieldMapping.Service_Cloud_Attribute__c) && populatedFields.containsKey(thisFieldMapping.Service_Cloud_Attribute__c)) {
237+
a.put(thisFieldMapping.Service_Cloud_Attribute__c, contact.get(thisFieldMapping.Service_Cloud_Attribute__c));
238+
}
239+
}
240+
241+
return a;
242+
}
243+
208244
/**
209245
* @description This method compares the "before" and "after" version of a processed sObject and evaluates
210246
* if any updates were made to the record. It does this by iterating over the collection of field mappings

src/sfdc/base/main/default/classes/B2CConstant.cls

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public with sharing class B2CConstant {
4747
'[{0}]; please verify that this storefront is defined and active.',
4848
ERRORS_META_CONTACTNOTFOUND = '--> B2C MetaData --> No Contact found mapped to Id [{0}]; please verify that ' +
4949
'this Contact record is defined.',
50+
ERRORS_META_ACCOUNTNOTFOUND = '--> B2C MetaData --> No Account found mapped to Id [{0}]; please verify that ' +
51+
'this Account record is defined.',
5052

5153
// Define the account / contact short-hand model names and mapping objects
5254
ACCOUNTCONTACTMODEL_STANDARD = 'Standard',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* @author Abraham David Lloyd
3+
* @date February 11th, 2021
4+
*
5+
* @description This class is used to retrieve B2C Commerce customer data and details
6+
* from custom object definitions. Each customer should also have an associated
7+
* default customerList.
8+
*/
9+
public with sharing class B2CContactAccountManager extends B2CBaseMeta {
10+
11+
/**
12+
* @description Attempts to retrieve a Contact configured via custom objects.
13+
*
14+
* @param contactId {String} Describes the Contact identifier used to retrieve a given definition
15+
* @param returnEmptyObject {Boolean} Describes if an empty sObject should be returned if no results are found
16+
* @param fieldMappings {List<B2C_Integration_Field_Mappings__mdt>} Represents the fieldMappings
17+
* @return {Account} Returns an instance of a Contact
18+
*/
19+
public static Account getAccountById(
20+
String accountId, Boolean returnEmptyObject, List<B2C_Integration_Field_Mappings__mdt> fieldMappings
21+
) {
22+
23+
// Initialize local variables
24+
List<Account> accounts;
25+
String errorMsg;
26+
Query accountQuery;
27+
Account output;
28+
29+
// Default the error message
30+
errorMsg = B2CConstant.buildErrorMessage(B2CConstant.ERRORS_META_ACCOUNTNOTFOUND, accountId);
31+
32+
// Seed the default query structure to leverage
33+
accountQuery = getDefaultQuery(fieldMappings);
34+
35+
// Define the record limit for the query
36+
accountQuery.setLimit(1);
37+
38+
// Define the default where-clause for the query
39+
accountQuery.addConditionEq('Id', accountId);
40+
41+
// Execute the query and evaluate the results
42+
accounts = accountQuery.run();
43+
44+
// Process the return results in a consistent manner
45+
output = (Account)processReturnResult('Account', returnEmptyObject, accounts, errorMsg);
46+
47+
// Return the customerList result
48+
return output;
49+
50+
}
51+
52+
/**
53+
* @description Helper function that takes an existing contact, and fieldMappings -- and creates an
54+
* object representation only containing mapped B2C Commerce properties that can be updated via the
55+
* OCAPI Data REST API.
56+
*
57+
* @param customerProfile {Account} Represents the account being processed for B2C Commerce updates
58+
* @param fieldMappings {List<B2C_Integration_Field_Mappings__mdt>} Represents the collection of
59+
* fieldMappings being evaluated
60+
* @return {Map<String, Object>} Returns an object representation of the properties to update
61+
*/
62+
public static Map<String, Object> getPublishProfile(
63+
Account customerProfile, List<B2C_Integration_Field_Mappings__mdt> fieldMappings, Map<String, Object> contactBasedMap
64+
) {
65+
66+
// Initialize local variables
67+
Map<String, Object> output;
68+
List<String> deleteNode;
69+
Object accountPropertyValue;
70+
String oCAPISubKey;
71+
72+
// Initialize the output map
73+
output = contactBasedMap.clone();
74+
deleteNode = new List<String>();
75+
76+
// Attach the contact and account Ids to the profile
77+
if (!contactBasedMap.containsKey('c_b2ccrm_contactId')) {
78+
output.put('c_b2ccrm_contactId', customerProfile.PersonContactId);
79+
}
80+
if (!contactBasedMap.containsKey('c_b2ccrm_accountId')) {
81+
output.put('c_b2ccrm_accountId', customerProfile.Id);
82+
}
83+
84+
// Loop over the collection of field mappings
85+
for (B2C_Integration_Field_Mappings__mdt thisFieldMapping: fieldMappings) {
86+
// Ensure contact-based mapping has priority on account-based fields
87+
if (output.containsKey(thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c)) {
88+
continue;
89+
}
90+
91+
// Create a reference to the property value for this contact
92+
accountPropertyValue = customerProfile.get(thisFieldMapping.Service_Cloud_Attribute__c);
93+
94+
// Is this property empty and is this not a child node? If so, then add it to the delete node
95+
if (accountPropertyValue == null && !thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c.contains('.')) {
96+
97+
// If so, then add it to the delete node (fields to clear out)
98+
deleteNode.add(thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c);
99+
100+
} else {
101+
102+
// Otherwise, attach the OCAPI property value to the object root
103+
output.put(thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c, accountPropertyValue);
104+
105+
}
106+
107+
}
108+
109+
// Do we have properties to delete? If so, then include it in the output
110+
if (deleteNode.size() > 0) {
111+
if (!output.containsKey('_delete')) {
112+
output.put('_delete', new List<String>());
113+
}
114+
115+
((List<String>)output.get('_delete')).addAll(deleteNode);
116+
}
117+
118+
// Returns the output collection
119+
return output;
120+
121+
}
122+
123+
/**
124+
* @description Helper method that provides a consistent set of columns to leverage
125+
* when selecting sObject data via SOQL
126+
*
127+
* @param fieldMappings {List<B2C_Integration_Field_Mappings__mdt>} Represents the fieldMappings
128+
* @return {Query} Returns the query template to leverage for customerLists
129+
*/
130+
private static Query getDefaultQuery(List<B2C_Integration_Field_Mappings__mdt> fieldMappings) {
131+
132+
// Initialize local variables
133+
Query accountQuery;
134+
135+
// Create the profile query that will be used to drive resolution
136+
accountQuery = new Query('Account');
137+
138+
// Add the base fields to retrieve (identifiers first)
139+
accountQuery.selectField('Id');
140+
141+
// Iterate over the field mappings and attach the mapped fields to the query
142+
for (B2C_Integration_Field_Mappings__mdt thisFieldMapping: fieldMappings) {
143+
144+
// Add the Salesforce Platform attribute to the query
145+
accountQuery.selectField(thisFieldMapping.Service_Cloud_Attribute__c);
146+
147+
}
148+
149+
// Return the default query structure
150+
return accountQuery;
151+
}
152+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>54.0</apiVersion>
4+
<status>Active</status>
5+
</ApexClass>

0 commit comments

Comments
 (0)