diff --git a/README.md b/README.md index 0adb773a..d930dcf8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Salesforce B2C Commerce / CRM Sync is an enablement solution designed by Salesfo 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. -> :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: +> :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: 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. @@ -24,6 +24,12 @@ b2c-crm-sync enables the resolution, synchronization, viewing, and management of 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. +Please find hereafter diagrams that shows how b2c-crm-sync is synching customer profile profiles in a bidirectional way: + +![B2C To Core Diagram](docs/imgs/B2CtoCore.png "B2C To Core Diagram") + +![Core To B2C Diagram](docs/imgs/CoretoB2C.png "Core To B2C Diagram") + ### License This project, its source code, and sample assets are all licensed under the [BSD 3-Clause](License.md) License. @@ -56,6 +62,20 @@ b2c-crm-sync supports the following extensible features (yes, you can customize > 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 :) +### Account-level attribute matching & mapping + +Starting since v4.0.0, we introduced a new level of data matching and data mapping between B2C Commerce and the Salesforce Core Platform. +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. +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. + +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: + +![B2C To Core - Account Level Mapping Diagram](docs/imgs/B2CtoCore-AccountLevelMapping.png "B2C To Core - Account Level Mapping Diagram") + +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. + +> 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. + ## Setup Guidance ### Deployment Considerations @@ -969,6 +989,12 @@ npm run crm-sync:sf:connectedapps ``` 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. +> 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 +- Go to settings +- in the search, look for OAuth +- select OAuth and OpenID Connect Settings under identity +- Turn on `Allow OAuth Username-Password Flows` + #### Create and Deploy Your Duplicate Rules 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: @@ -1440,7 +1466,7 @@ As the B2C Commerce customer profile and its addresses are fetched by the core p 3. In the Quick Find box at the top left, search for `Custom Metadata` and click on this menu 4. Click on the `Manage records` on the row `B2C Integration Field Mapping` 5. You'll find all the data mapping here, that you can modify, remove, or add new attributes as part of the data mapping. - +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. | Label | Core Object | Core ID | Core Alt ID | B2C Object | OCAPI ID | |:-------------------:|:----------------------:|:--------------------------:|:---------------------------:|:---------------:|:-------------------:| diff --git a/docs/imgs/B2CtoCore-AccountLevelMapping.png b/docs/imgs/B2CtoCore-AccountLevelMapping.png new file mode 100644 index 00000000..aac7ca04 Binary files /dev/null and b/docs/imgs/B2CtoCore-AccountLevelMapping.png differ diff --git a/docs/imgs/B2CtoCore.png b/docs/imgs/B2CtoCore.png new file mode 100644 index 00000000..c43cba2e Binary files /dev/null and b/docs/imgs/B2CtoCore.png differ diff --git a/docs/imgs/CoretoB2C.png b/docs/imgs/CoretoB2C.png new file mode 100644 index 00000000..93fc6958 Binary files /dev/null and b/docs/imgs/CoretoB2C.png differ diff --git a/src/sfdc/base/main/default/classes/B2CBaseAttributeAssignment.cls b/src/sfdc/base/main/default/classes/B2CBaseAttributeAssignment.cls index 11e27ae7..17f2d874 100644 --- a/src/sfdc/base/main/default/classes/B2CBaseAttributeAssignment.cls +++ b/src/sfdc/base/main/default/classes/B2CBaseAttributeAssignment.cls @@ -123,7 +123,7 @@ public abstract with sharing class B2CBaseAttributeAssignment { if (sourceObject.isSet(objectFieldName) && sourceObject.get(objectFieldName) != null) { // If so, then evaluate if the targetObject is missing this field -- or it has it, and the value is null - if (!targetObjectFields.contains(objectFieldName) || (targetObjectFields.contains(objectFieldName) && targetObject.get(objectFieldName) == null)) { + if (doesFieldExist(getSchemaMap(targetObject), objectFieldName) && (!targetObjectFields.contains(objectFieldName) || (targetObjectFields.contains(objectFieldName) && targetObject.get(objectFieldName) == null))) { // If the field exists, see if it has been set in the target object if (targetObject.get(objectFieldName) == null) { @@ -205,6 +205,71 @@ public abstract with sharing class B2CBaseAttributeAssignment { } + /** + * @description This method returns true if the given contact sObject contains at least one account-level attribute value. + * + * @param contact {SObject} Represents the contact to translate + * @param accountFieldMappings {List} Represents the collection of field mappings to leverage for the account + * @return {Boolean} Returns true if the given contact contains at least one account-level attribute value + */ + public static Boolean hasAccountBasedAttributes(SObject contact, List accountFieldMappings) { + if (contact.getSObjectType().getDescribe().getName() == 'Contact') { + Map populatedFields = contact.getPopulatedFieldsAsMap(); + Account a = new Account(); + Contact realContact = (Contact)contact; + + for (B2C_Integration_Field_Mappings__mdt thisFieldMapping : accountFieldMappings) { + if (doesFieldExist(getSchemaMap(a), thisFieldMapping.Service_Cloud_Attribute__c) && !doesFieldExist(getSchemaMap(realContact), thisFieldMapping.Service_Cloud_Attribute__c) && populatedFields.containsKey(thisFieldMapping.Service_Cloud_Attribute__c)) { + return true; + } + } + } else { + for (B2C_Integration_Field_Mappings__mdt thisFieldMapping : accountFieldMappings) { + if (doesFieldExist(getSchemaMap(contact), thisFieldMapping.Service_Cloud_Attribute__c) && contact.isSet(thisFieldMapping.Service_Cloud_Attribute__c)) { + return true; + } + } + } + + return false; + } + + /** + * @description This method translates a Contact into a Person Account. It does this by leveraging the + * field mappings for the contact and account objects. + * + * @param contact {SObject} Represents the contact to translate + * @param contactFieldMappings {List} Represents the collection of field mappings to leverage for the contact + * @param accountFieldMappings {List} Represents the collection of field mappings to leverage for the account + * @return {Account} Returns the translated contact into a Person Account + */ + public static Account translateContactToPersonAccount(SObject contact, List contactFieldMappings, List accountFieldMappings) { + // Person Account Record Type + RecordType rt = [SELECT Id, DeveloperName FROM RecordType WHERE DeveloperName = :B2CConfigurationManager.getPersonAccountRecordTypeDeveloperName() WITH SECURITY_ENFORCED]; + Account a = new Account( + RecordTypeId = rt.Id + ); + Map populatedFields = contact.getPopulatedFieldsAsMap(); + + // Loop over the contact fieldMappings and evaluate each field + for (B2C_Integration_Field_Mappings__mdt thisFieldMapping : contactFieldMappings) { + // Compare the attribute values for the original and processed objects + 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)) { + a.put(thisFieldMapping.Service_Cloud_Attribute_Alt__c, contact.get(thisFieldMapping.Service_Cloud_Attribute__c)); + } + } + + // Loop over the account fieldMappings and evaluate each field + for (B2C_Integration_Field_Mappings__mdt thisFieldMapping : accountFieldMappings) { + // Compare the attribute values for the original and processed objects + if (doesFieldExist(getSchemaMap(a), thisFieldMapping.Service_Cloud_Attribute__c) && doesFieldExist(getSchemaMap(contact), thisFieldMapping.Service_Cloud_Attribute__c) && populatedFields.containsKey(thisFieldMapping.Service_Cloud_Attribute__c)) { + a.put(thisFieldMapping.Service_Cloud_Attribute__c, contact.get(thisFieldMapping.Service_Cloud_Attribute__c)); + } + } + + return a; + } + /** * @description This method compares the "before" and "after" version of a processed sObject and evaluates * if any updates were made to the record. It does this by iterating over the collection of field mappings diff --git a/src/sfdc/base/main/default/classes/B2CConstant.cls b/src/sfdc/base/main/default/classes/B2CConstant.cls index 6263c284..8c70efc2 100644 --- a/src/sfdc/base/main/default/classes/B2CConstant.cls +++ b/src/sfdc/base/main/default/classes/B2CConstant.cls @@ -47,6 +47,8 @@ public with sharing class B2CConstant { '[{0}]; please verify that this storefront is defined and active.', ERRORS_META_CONTACTNOTFOUND = '--> B2C MetaData --> No Contact found mapped to Id [{0}]; please verify that ' + 'this Contact record is defined.', + ERRORS_META_ACCOUNTNOTFOUND = '--> B2C MetaData --> No Account found mapped to Id [{0}]; please verify that ' + + 'this Account record is defined.', // Define the account / contact short-hand model names and mapping objects ACCOUNTCONTACTMODEL_STANDARD = 'Standard', diff --git a/src/sfdc/base/main/default/classes/B2CContactAccountManager.cls b/src/sfdc/base/main/default/classes/B2CContactAccountManager.cls new file mode 100644 index 00000000..64df8391 --- /dev/null +++ b/src/sfdc/base/main/default/classes/B2CContactAccountManager.cls @@ -0,0 +1,149 @@ +/** + * @author Abraham David Lloyd + * @date February 11th, 2021 + * + * @description This class is used to retrieve B2C Commerce customer data and details + * from custom object definitions. Each customer should also have an associated + * default customerList. + */ +public with sharing class B2CContactAccountManager extends B2CBaseMeta { + + /** + * @description Attempts to retrieve a Contact configured via custom objects. + * + * @param contactId {String} Describes the Contact identifier used to retrieve a given definition + * @param returnEmptyObject {Boolean} Describes if an empty sObject should be returned if no results are found + * @param fieldMappings {List} Represents the fieldMappings + * @return {Account} Returns an instance of a Contact + */ + public static Account getAccountById( + String accountId, Boolean returnEmptyObject, List fieldMappings + ) { + + // Initialize local variables + List accounts; + String errorMsg; + Query accountQuery; + Account output; + + // Default the error message + errorMsg = B2CConstant.buildErrorMessage(B2CConstant.ERRORS_META_ACCOUNTNOTFOUND, accountId); + + // Seed the default query structure to leverage + accountQuery = getDefaultQuery(fieldMappings); + + // Define the record limit for the query + accountQuery.setLimit(1); + + // Define the default where-clause for the query + accountQuery.addConditionEq('Id', accountId); + + // Execute the query and evaluate the results + accounts = accountQuery.run(); + + // Process the return results in a consistent manner + output = (Account)processReturnResult('Account', returnEmptyObject, accounts, errorMsg); + + // Return the customerList result + return output; + + } + + /** + * @description Helper function that takes an existing contact, and fieldMappings -- and creates an + * object representation only containing mapped B2C Commerce properties that can be updated via the + * OCAPI Data REST API. + * + * @param customerProfile {Account} Represents the account being processed for B2C Commerce updates + * @param fieldMappings {List} Represents the collection of + * fieldMappings being evaluated + * @return {Map} Returns an object representation of the properties to update + */ + public static Map getPublishProfile( + Account customerProfile, List fieldMappings, Map contactBasedMap + ) { + + // Initialize local variables + Map output; + List deleteNode; + Object accountPropertyValue; + String oCAPISubKey; + + // Initialize the output map + output = contactBasedMap.clone(); + deleteNode = new List(); + + // Attach the contact and account Ids to the profile + if (!contactBasedMap.containsKey('c_b2ccrm_accountId')) { + output.put('c_b2ccrm_accountId', customerProfile.Id); + } + + // Loop over the collection of field mappings + for (B2C_Integration_Field_Mappings__mdt thisFieldMapping: fieldMappings) { + // Ensure contact-based mapping has priority on account-based fields + if (output.containsKey(thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c)) { + continue; + } + + // Create a reference to the property value for this contact + accountPropertyValue = customerProfile.get(thisFieldMapping.Service_Cloud_Attribute__c); + + // Is this property empty and is this not a child node? If so, then add it to the delete node + if (accountPropertyValue == null && !thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c.contains('.')) { + + // If so, then add it to the delete node (fields to clear out) + deleteNode.add(thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c); + + } else { + + // Otherwise, attach the OCAPI property value to the object root + output.put(thisFieldMapping.B2C_Commerce_OCAPI_Attribute__c, accountPropertyValue); + + } + + } + + // Do we have properties to delete? If so, then include it in the output + if (deleteNode.size() > 0) { + if (!contactBasedMap.containsKey('_delete')) { + contactBasedMap.put('_delete', new List()); + } + + ((List)contactBasedMap.get('_delete')).addAll(deleteNode); + } + + // Returns the output collection + return output; + + } + + /** + * @description Helper method that provides a consistent set of columns to leverage + * when selecting sObject data via SOQL + * + * @param fieldMappings {List} Represents the fieldMappings + * @return {Query} Returns the query template to leverage for customerLists + */ + private static Query getDefaultQuery(List fieldMappings) { + + // Initialize local variables + Query accountQuery; + + // Create the profile query that will be used to drive resolution + accountQuery = new Query('Account'); + + // Add the base fields to retrieve (identifiers first) + accountQuery.selectField('Id'); + + // Iterate over the field mappings and attach the mapped fields to the query + for (B2C_Integration_Field_Mappings__mdt thisFieldMapping: fieldMappings) { + + // Add the Salesforce Platform attribute to the query + accountQuery.selectField(thisFieldMapping.Service_Cloud_Attribute__c); + + } + + // Return the default query structure + return accountQuery; + } +} diff --git a/src/sfdc/base/main/default/classes/B2CContactAccountManager.cls-meta.xml b/src/sfdc/base/main/default/classes/B2CContactAccountManager.cls-meta.xml new file mode 100644 index 00000000..40d67933 --- /dev/null +++ b/src/sfdc/base/main/default/classes/B2CContactAccountManager.cls-meta.xml @@ -0,0 +1,5 @@ + + + 54.0 + Active + diff --git a/src/sfdc/base/main/default/classes/B2CIACustomerResolution.cls b/src/sfdc/base/main/default/classes/B2CIACustomerResolution.cls index 60383304..29cc82cc 100644 --- a/src/sfdc/base/main/default/classes/B2CIACustomerResolution.cls +++ b/src/sfdc/base/main/default/classes/B2CIACustomerResolution.cls @@ -17,12 +17,14 @@ public inherited sharing class B2CIACustomerResolution { * only 1 item in the main list to support use with Flow. */ @InvocableMethod(Label='B2C: Customer Resolution' Description='Finds matching contacts based on B2C matching rules') - public static List resolve(List> contactList) { + public static List resolve(List> contactList) { // Initialize local variables B2CIACustomerResolutionResult resolutionResults; List output; - List contactsToResolve; + List contactsToResolve; + List contactFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Contact'); + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Account'); // Initialize the output variable output = new List(); @@ -55,12 +57,12 @@ public inherited sharing class B2CIACustomerResolution { } else { // Loop over the collection of contacts to resolve - for (Contact c: contactsToResolve) { + for (SObject c: contactsToResolve) { try { // Add the resolution results to the output class - resolutionResults.contactList = findDupes(c); + resolutionResults.contactList = findDupes(c, contactFieldMappings, accountFieldMappings); } catch (System.HandledException e) { @@ -92,51 +94,63 @@ public inherited sharing class B2CIACustomerResolution { * @param pContact {Contact} A specific contact to run find duplicates on * @return {List} The final list of contacts that exist that are relevant duplicates */ - public static List findDupes(Contact pContact) { + public static List findDupes(SObject pContact, List contactFieldMappings, List accountFieldMappings) { // Initialize local variables - Contact contactToResolve; + List contactToResolve = new List(); + List accountToResolve = new List(); List matchedContactRecords = new List(); List duplicateContactRecords = new List(); List matchedAccountRecords = new List(); String accountContactModel; + Datacloud.FindDuplicatesResult[] results; + + if (B2CConfigurationManager.getDefaultAccountContactModel() == B2CConstant.ACCOUNTCONTACTMODEL_PERSON + && accountFieldMappings.size() > 0 + && B2CBaseAttributeAssignment.hasAccountBasedAttributes(pcontact, accountFieldMappings)) { // PersonAccount enabled and Account-based attributes passed in + System.debug(LoggingLevel.DEBUG, '--> PersonAccount-based record sent for resolution'); + + // Because there is at least one account-based attribute passed in, we need to convert the Contact to a PersonAccount so that the PersonAccount duplicate rule is triggered, and not the Contact one + // This is because the Contact duplicate rule cannot map to an Account level attribute when running the duplicate rule + Account a = B2CBaseAttributeAssignment.translateContactToPersonAccount(pContact, contactFieldMappings, accountFieldMappings); + accountToResolve.add(a); + results = Datacloud.FindDuplicates.findDuplicates(accountToResolve); + } else { // Contact passed in + System.debug(LoggingLevel.DEBUG, '--> Contact-based record sent for resolution'); + + contactToResolve.add((Contact)pContact.clone(true)); + + // Has the lastName been defined? + if (!contactToResolve.get(0).isSet('LastName') || contactToResolve.get(0).get('LastName') == null) { + // Default the lastName if it's not set -- solely for the purpose of resolution + contactToResolve.get(0).put('LastName', B2CConfigurationManager.getDefaultAccountContactNames().get('contactName')); + } - // Make a copy of the contact passed-in - contactToResolve = pContact.clone(true); - - // Has the lastName been defined? - if (contactToResolve.LastName == null) { - - // Default the lastName if it's not set -- solely for the purpose of resolution - contactToResolve.LastName = B2CConfigurationManager.getDefaultAccountContactNames().get('contactName'); - + results = Datacloud.FindDuplicates.findDuplicates(contactToResolve); } - // Retrieve the duplicate / related contacts driven by configured matchRules - Datacloud.FindDuplicatesResult[] results = Datacloud.FindDuplicates.findDuplicates( - new List{contactToResolve} - ); + System.debug(LoggingLevel.DEBUG, '--> Iterating over DataCloud results (' + results.size() + ')'); //If so, iterate over them and start checking for potential matches for (Datacloud.FindDuplicatesResult fdrI : results) { - System.debug(LoggingLevel.DEBUG, '--> Iterating Over DataCloud results (' + results.size() + ')'); + System.debug(LoggingLevel.DEBUG, '--> FindDuplicatesResult: ' + fdrI); + System.debug(LoggingLevel.DEBUG, '--> Iterating over duplicate rules results (' + fdrI.getDuplicateResults() + ')'); // find duplicates based on what was passed in for (Datacloud.DuplicateResult dupeResultI : fdrI.getDuplicateResults()) { - System.debug(LoggingLevel.DEBUG, '--> Iterating over duplicateRule results'); + System.debug(LoggingLevel.DEBUG, '--> DuplicateResult: ' + dupeResultI); + System.debug(LoggingLevel.DEBUG, '--> Iterating over matching rule results (' + dupeResultI.getMatchResults() + ')'); // Iterate over the collection of match / duplicate rules for (Datacloud.MatchResult matchResultI : dupeResultI.getMatchResults()) { - System.debug(LoggingLevel.DEBUG, '--> Iterating over MatchRule results'); - // Create a reference to the current rule being processed String matchRule = matchResultI.getRule(); System.debug(LoggingLevel.DEBUG, '--> MatchRule: ' + matchRule); - System.debug(LoggingLevel.DEBUG, '--> Matched RecordCount: ' + matchResultI.getMatchRecords().size()); + System.debug(LoggingLevel.DEBUG, '--> Iterating over matching rule records (' + matchResultI.getMatchRecords().size() + ')'); // Instead of processing all rules, we only want to process B2C Commerce-specific rules if (matchRule.contains('B2C') && matchResultI.getMatchRecords().size() > 0) { @@ -144,6 +158,8 @@ public inherited sharing class B2CIACustomerResolution { // Loop over the collection of match records for (Datacloud.MatchRecord dmrI : matchResultI.getMatchRecords()) { + System.debug(LoggingLevel.DEBUG, '--> MatchRecord: ' + dmrI); + // Capture the accountContactModel being leveraged accountContactModel = B2C_CRMSync_Setting__mdt.getInstance( 'Default_Configuration').Account_Contact_Model__c; diff --git a/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile.cls b/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile.cls index 77688fb2..9efef47e 100644 --- a/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile.cls +++ b/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile.cls @@ -19,22 +19,21 @@ public with sharing class B2CIAProcessCustomerProfile extends B2CBaseAttributeAs // Initialize local variables JSONParse customerProfileJSON; - List customerProfiles; + List customerProfiles = new List(); + List customerProfilesAccounts = new List(); SObject thisCustomerProfile; + SObject thisCustomerProfileAccount; SObject originalCustomerProfile; - List fieldMappings; + SObject originalCustomerProfileAccount; + // Retrieve the fieldMappings to leverage based on the Account / Contact Model being employed + List contactFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Contact'); + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Account'); + Boolean hasSObjectBeenUpdated; + Boolean hasSObjectBeenUpdatedAccount; Integer totalContactUpdates; - String accountContactModel; - // Start by retrieving the default account / contactModel - accountContactModel = B2CConfigurationManager.getDefaultAccountContactModel(); - - // Initialize the customerProfile collection - customerProfiles = new List(); - - // Retrieve the fieldMappings to leverage based on the Account / Contact Model being employed - fieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Contact'); + String accountContactModel = B2CConfigurationManager.getDefaultAccountContactModel(); // Iterate over the collection of customerProfile results for (B2CIAGetCustomerProfileResult thisCustomerProfileResult : customerProfileResults) { @@ -44,33 +43,39 @@ public with sharing class B2CIAProcessCustomerProfile extends B2CBaseAttributeAs // Default the tracking flag hasSObjectBeenUpdated = false; + hasSObjectBeenUpdatedAccount = false; // Deserialize the REST API response into a generic and typed object customerProfileJSON = new JSONParse(thisCustomerProfileResult.responseBody); // Retrieve the current customerProfile using the specified crmId - thisCustomerProfile = B2CContactManager.getContactById(thisCustomerProfileResult.crmContactId, true, fieldMappings); + thisCustomerProfile = B2CContactManager.getContactById(thisCustomerProfileResult.crmContactId, true, contactFieldMappings); + String accountId = (String)thisCustomerProfile.get('AccountId'); // Are we currently using the personAccount accountContact model? if (accountContactModel == B2CConstant.ACCOUNTCONTACTMODEL_PERSON) { - // Remove the AccountID as we can't update it when the person customerModel is employed thisCustomerProfile = B2CBaseAttributeAssignment.removePersonAccountProperties(thisCustomerProfile); - } // Create a copy of the cloned customerProfile -- so that we can compare specific values originalCustomerProfile = thisCustomerProfile.clone(true, true, true, true); // Update the key properties of the current object - thisCustomerProfile = applyMappedFieldValues(thisCustomerProfile, customerProfileJSON, fieldMappings); + thisCustomerProfile = applyMappedFieldValues(thisCustomerProfile, customerProfileJSON, contactFieldMappings); // Evaluate the updates made and determine if the object has been updated - hasSObjectBeenUpdated = hasSObjectBeenUpdated(originalCustomerProfile, thisCustomerProfile, fieldMappings); + hasSObjectBeenUpdated = hasSObjectBeenUpdated(originalCustomerProfile, thisCustomerProfile, contactFieldMappings); - // Has the object been updated? - if (hasSObjectBeenUpdated == true) { + if (String.isNotEmpty(accountId)) { + thisCustomerProfileAccount = B2CContactAccountManager.getAccountById(accountId, true, accountFieldMappings); + originalCustomerProfileAccount = thisCustomerProfileAccount.clone(true, true, true, true); + thisCustomerProfileAccount = applyMappedFieldValues(thisCustomerProfileAccount, customerProfileJSON, accountFieldMappings); + hasSObjectBeenUpdatedAccount = hasSObjectBeenUpdated(originalCustomerProfileAccount, thisCustomerProfileAccount, accountFieldMappings); + } + // Has the object been updated? + if (hasSObjectBeenUpdated == true || hasSObjectBeenUpdatedAccount == true) { // Audit that the PlatformEvent applied updates and record the date thisCustomerProfile.put('Last_Platform_Event_Processed_Date__c', System.Datetime.now()); thisCustomerProfile.put('Last_B2C_Commerce_Update_Processed__c', System.Datetime.now()); @@ -84,39 +89,46 @@ public with sharing class B2CIAProcessCustomerProfile extends B2CBaseAttributeAs thisCustomerProfile.put('Updated_by_B2C_Platform_Event__c', true); // Append the customerProfile to the processing collection - customerProfiles.add(thisCustomerProfile); - + if (hasSObjectBeenUpdated == true) { + customerProfiles.add(thisCustomerProfile); + } + if (hasSObjectBeenUpdatedAccount == true) { + customerProfilesAccounts.add(thisCustomerProfileAccount); + } } else { - // Audit that the PlatformEvent did not apply updates originalCustomerProfile.put('Last_Platform_Event_Processed_Date__c', System.Datetime.now()); originalCustomerProfile.put('Last_Platform_Event_Applied_Updates__c', false); // Append the customerProfile to the processing collection customerProfiles.add(originalCustomerProfile); - } - } // Was at least one customerProfile record processed? - if (Contact.SObjectType.getDescribe().isUpdateable() && - Contact.SObjectType.getDescribe().isCreateable() && - Contact.SObjectType.getDescribe().isAccessible() && - Schema.SObjectType.Contact.fields.LastName.isUpdateable() && - Schema.SObjectType.Contact.fields.LastName.isCreateable() && - customerProfiles.size() > 0) { - + if (customerProfiles.size() > 0 + && (Contact.SObjectType.getDescribe().isUpdateable() && + Contact.SObjectType.getDescribe().isCreateable() && + Contact.SObjectType.getDescribe().isAccessible() && + Schema.SObjectType.Contact.fields.LastName.isUpdateable() && + Schema.SObjectType.Contact.fields.LastName.isCreateable())) { // If so, then process the customerProfiles - - Map accmap = new Map(); - - accmap.putall(customerProfiles); - - upsert accmap.values(); - + Map contactMap = new Map(); + contactMap.putall(customerProfiles); + upsert contactMap.values(); } + // Was at least one customerProfileAccount record processed? + if (customerProfilesAccounts.size() > 0 + && (Account.SObjectType.getDescribe().isUpdateable() && + Account.SObjectType.getDescribe().isCreateable() && + Account.SObjectType.getDescribe().isAccessible() && + Schema.SObjectType.Account.fields.LastName.isUpdateable() && + Schema.SObjectType.Account.fields.LastName.isCreateable())) { + // If so, then process the customerProfiles + Map accountMap = new Map(); + accountMap.putall(customerProfilesAccounts); + upsert accountMap.values(); + } } - } diff --git a/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile_Test.cls b/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile_Test.cls index f3405831..c164ecb2 100644 --- a/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile_Test.cls +++ b/src/sfdc/base/main/default/classes/B2CIAProcessCustomerProfile_Test.cls @@ -78,4 +78,55 @@ private class B2CIAProcessCustomerProfile_Test { } + @IsTest + static void testProcessCustomerProfileAccountUpdateSuccess() { + + // Initialize local variables + List requestArguments = new List(); + B2CIAGetCustomerProfileResult input = new B2CIAGetCustomerProfileResult(); + + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Account'); + if (accountFieldMappings.size() == 0) { + Assert.isTrue(true, 'No Account based fields mapping, abort this test.'); + return; + } + + // Create a test account that we'll exercise + Account a = (Account)TestDataFactory.createSObject('Account', new Map{ + 'Name' => 'Name', + accountFieldMappings.get(0).Service_Cloud_Attribute__c => 'OriginalValue', + 'RecordTypeId' => B2CIACustomerResolution_TestHelper.getRecordType(B2CConfigurationManager.getAccountRecordTypeDeveloperName()).Id + }); + + // Create a test contact that we'll exercise + Contact c = (Contact)TestDataFactory.createSObject('Contact', new Map{ + 'AccountId' => a.Id, + 'FirstName' => 'firstName', + 'LastName' => 'originalLastName', + 'Email' => 'test-user@b2csa.qa.salesforce.com' + }); + + // Seed the request arguments + input.crmContactId = c.Id; + input.responseBody = '{"' + accountFieldMappings.get(0).B2C_Commerce_OCAPI_Attribute__c + '": "updatedValue"}'; + requestArguments.add(input); + + Test.startTest(); + + // Attempt to update the customerProfiles using the properties / specified + B2CIAProcessCustomerProfile.updateCustomerProfiles(requestArguments); + + // Retrieve the updatedAccount using the same fields + String query = 'SELECT Id, ' + accountFieldMappings.get(0).Service_Cloud_Attribute__c + ' FROM Account WHERE Id = :accountId LIMIT 1'; + Account updatedAccount = Database.queryWithBinds(query, new Map { + 'accountId' => a.Id + }, AccessLevel.SYSTEM_MODE); + + Test.stopTest(); + + // Validate that the specific customerProfile updates were processed successfully + System.assertEquals(updatedAccount.get(accountFieldMappings.get(0).Service_Cloud_Attribute__c), 'updatedValue', 'Expected the site for the account to be updated with the responseBody contents'); + + } + } diff --git a/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile.cls b/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile.cls index 288a8473..30b4f48b 100644 --- a/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile.cls +++ b/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile.cls @@ -20,51 +20,53 @@ public with sharing class B2CIAPublishCustomerProfile extends B2CBaseAttributeAs // Initialize local variables Contact thisCustomerProfile; + Account thisCustomerProfileAccount; SObject thisCustomerProfileToUpdate; Integer totalUpdates; Map thisB2CProfile; - List contactsToUpdate; - List fieldMappings; - List output; + List contactsToUpdate = new List(); + // Get the fieldMappings for the customerProfile object + List contactFieldMappings = B2CMetaFieldMappings.getFieldMappingsForPublishing('Contact'); + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForPublishing('Account'); + List output = new List(); B2CIAPublishCustomerProfileResult customerPublishResult; String thisB2CProfileJSON; JSONParse parsedJSON; JSONParse parsedBodyJSON; - String accountContactModel; + // Start by retrieving the default account / contactModel + String accountContactModel = B2CConfigurationManager.getDefaultAccountContactModel(); // Initialize the request properties HttpRequest req; Http https; HttpResponse res; - // Start by retrieving the default account / contactModel - accountContactModel = B2CConfigurationManager.getDefaultAccountContactModel(); - - // Initialize the output variable - output = new List(); - contactsToUpdate = new List(); - - // Get the fieldMappings for the customerProfile object - fieldMappings = B2CMetaFieldMappings.getFieldMappingsForPublishing('Contact'); - // Iterate over the collection of customerProfile results for (B2CIAPublishCustomerProfileInput requestInput : requestArguments) { // Create a reference to the current CRM Contact thisCustomerProfile = requestInput.crmContact; - - // If not, get the publish profile for the current contact record - thisB2CProfile = B2CContactManager.getPublishProfile(thisCustomerProfile, fieldMappings); - - // Serialize the B2C Profile as a JSON object - thisB2CProfileJSON = JSON.serializePretty(thisB2CProfile, true); + String accountId = (String)thisCustomerProfile.AccountId; + if (!String.isEmpty(accountId)) { + thisCustomerProfileAccount = B2CContactAccountManager.getAccountById(accountId, true, accountFieldMappings); + } // Was a pre-defined profileJSON included in the event? if (requestInput.b2cProfileJSON != null && requestInput.b2cProfileJSON.length() > 0) { // Otherwise, leverage the pre-defined profileJSON in the response thisB2CProfileJSON = requestInput.b2cProfileJSON; + } else { + // If the account record was found, add up the account-based fields too + if (thisCustomerProfileAccount != null) { + thisB2CProfile = B2CContactAccountManager.getPublishProfile(thisCustomerProfileAccount, accountFieldMappings, B2CContactManager.getPublishProfile(thisCustomerProfile, contactFieldMappings)); + } else { + // If not, get the publish profile for the current contact record + thisB2CProfile = B2CContactManager.getPublishProfile(thisCustomerProfile, contactFieldMappings); + } + // Serialize the B2C Profile as a JSON object + thisB2CProfileJSON = JSON.serializePretty(thisB2CProfile, true); } // Build the request object utilizing the input properties diff --git a/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile_Test.cls b/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile_Test.cls index 2059f4e5..ddb03a8e 100644 --- a/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile_Test.cls +++ b/src/sfdc/base/main/default/classes/B2CIAPublishCustomerProfile_Test.cls @@ -63,6 +63,60 @@ private class B2CIAPublishCustomerProfile_Test { } + /** + * @see B2CIAValidateContact.validateContact + * @description This test method exercises publishEvents and verifies that successful events + * are processed correctly. We expect a 200 http-status to be included in the response. + */ + @IsTest + static void publishEventSucceededWithCalculatedBody() { + + // Initialize local variables + List inputList = new List(); + List publishResult = new List(); + B2CIAPublishCustomerProfileInput input = new B2CIAPublishCustomerProfileInput(); + + // Initialize and create the test contact + Contact c = new Contact( + LastName = 'lastname', + Total_Updates_to_B2C_Commerce__c = 0, + Last_Update_Pushed_to_B2C_Commerce__c = System.Datetime.now(), + Last_Platform_Event_Applied_Updates__c = false, + Last_Platform_Event_Processed_Date__c = System.Datetime.now() + ); + + Database.insert( c ); + + // Seed the publishEvent properties + input.apiVersion = 'apiVersion'; + input.b2cCustomerListId = 'id'; + input.b2cCustomerNo = 'customerno'; + input.b2cCustomerId = 'customerid'; + input.crmCustomerListId = 'crmCustomerListId'; + input.crmContactId = c.Id; + input.crmContact = c; + + // Add the publishEvent to the collection + inputList.add( input ); + + Test.startTest(); + + // Initialize the mock and execute the test + Test.setMock(HttpCalloutMock.class, new B2CHttpTestCalloutMockGenerator('CustomerDetailsSuccess')); + publishResult = B2CIAPublishCustomerProfile.publishCustomerProfile(inputList); + + Test.stopTest(); + + // Iterate over the collection of publish results + for (B2CIAPublishCustomerProfileResult thisResult : publishResult) { + + // Validate that the results were processed successfully + System.assertEquals(thisResult.statusCode, 200, 'Expected the statusCode to equal 200 -- indicating a successful publishingEvent'); + + } + + } + /** * @see B2CIAValidateContact.validateContact * @description This test method exercises publishEvents and verifies that successful events @@ -118,4 +172,74 @@ private class B2CIAPublishCustomerProfile_Test { } + /** + * @see B2CIAValidateContact.validateContact + * @description This test method exercises publishEvents and verifies that successful events + * are processed correctly. We expect a 200 http-status to be included in the response. + */ + @IsTest + static void publishEventWithMergedContactAndAccountSucceeded() { + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Account'); + if (accountFieldMappings.size() == 0) { + Assert.isTrue(true, 'No Account based fields mapping, abort this test.'); + return; + } + + // Initialize local variables + List inputList = new List(); + List publishResult = new List(); + B2CIAPublishCustomerProfileInput input = new B2CIAPublishCustomerProfileInput(); + + // Create a test account that we'll exercise + Account a = (Account)TestDataFactory.createSObject('Account', new Map{ + 'Name' => 'Name', + accountFieldMappings.get(0).Service_Cloud_Attribute__c => 'OriginalValue', + 'RecordTypeId' => B2CIACustomerResolution_TestHelper.getRecordType(B2CConfigurationManager.getAccountRecordTypeDeveloperName()).Id + }); + + // Initialize and create the test contact + Contact c = new Contact( + AccountId = a.Id, + LastName = 'lastname', + Total_Updates_to_B2C_Commerce__c = 0, + Last_Update_Pushed_to_B2C_Commerce__c = System.Datetime.now(), + Last_Platform_Event_Applied_Updates__c = false, + Last_Platform_Event_Processed_Date__c = System.Datetime.now() + ); + + Database.insert(c); + + // Seed the publishEvent properties + input.apiVersion = 'apiVersion'; + input.b2cCustomerListId = 'id'; + input.b2cCustomerNo = 'customerno'; + input.b2cCustomerId = 'customerid'; + input.crmCustomerListId = 'crmCustomerListId'; + input.crmContactId = c.Id; + input.crmContact = c; + + // Add the publishEvent to the collection + inputList.add( input ); + + Test.startTest(); + + // Initialize the mock and execute the test + Test.setMock(HttpCalloutMock.class, new B2CHttpTestCalloutMockGenerator('CustomerDetailsSuccess')); + publishResult = B2CIAPublishCustomerProfile.publishCustomerProfile(inputList); + + Test.stopTest(); + + // Iterate over the collection of publish results + for (B2CIAPublishCustomerProfileResult thisResult : publishResult) { + + // Validate that the results were processed successfully + Assert.areEqual(200, thisResult.statusCode, 'Expected the statusCode to equal 200 -- indicating a successful publishingEvent'); + + // Validate that the JSON body is a merge of both Account & Contact properties + Assert.areEqual('OriginalValue', thisResult.b2cCustomerProfile.get(accountFieldMappings.get(0).B2C_Commerce_OCAPI_Attribute__c).getStringValue(), 'Expected the account value to be part of the b2c profile JSON'); + + } + + } + } diff --git a/src/sfdc/base/main/default/classes/B2CIASynchronizeContact.cls b/src/sfdc/base/main/default/classes/B2CIASynchronizeContact.cls index 3fa6ef0a..6085aada 100644 --- a/src/sfdc/base/main/default/classes/B2CIASynchronizeContact.cls +++ b/src/sfdc/base/main/default/classes/B2CIASynchronizeContact.cls @@ -39,9 +39,8 @@ public with sharing class B2CIASynchronizeContact extends B2CBaseAttributeAssign // Loop over the collection of input results for (B2CIASynchronizeContactInput contactToProcess: contactsToProcess) { - // First, convert the contacts to sObjects - sourceContact = (SObject) contactToProcess.sourceContact; - targetContact = (SObject) contactToProcess.targetContact; + sourceContact = contactToProcess.sourceContact; + targetContact = contactToProcess.targetContact; clonedTargetContact = targetContact.clone(true, true); // Audit the source / target contacts before processing takes place diff --git a/src/sfdc/base/main/default/classes/B2CIASynchronizeContactInput.cls b/src/sfdc/base/main/default/classes/B2CIASynchronizeContactInput.cls index 8dbbb6e4..62849a9f 100644 --- a/src/sfdc/base/main/default/classes/B2CIASynchronizeContactInput.cls +++ b/src/sfdc/base/main/default/classes/B2CIASynchronizeContactInput.cls @@ -13,9 +13,9 @@ public class B2CIASynchronizeContactInput { //////////////////////////////////////////////////////////////// @InvocableVariable - public Contact sourceContact; + public SObject sourceContact; @InvocableVariable - public Contact targetContact; + public SObject targetContact; } diff --git a/src/sfdc/base/main/default/classes/B2CIASynchronizeContactResult.cls b/src/sfdc/base/main/default/classes/B2CIASynchronizeContactResult.cls index db2e30f4..63f64570 100644 --- a/src/sfdc/base/main/default/classes/B2CIASynchronizeContactResult.cls +++ b/src/sfdc/base/main/default/classes/B2CIASynchronizeContactResult.cls @@ -13,10 +13,10 @@ public class B2CIASynchronizeContactResult { //////////////////////////////////////////////////////////////// @InvocableVariable - public Contact sourceContact; + public SObject sourceContact; @InvocableVariable - public Contact originalTargetContact; + public SObject originalTargetContact; @InvocableVariable public List missingContactFields; diff --git a/src/sfdc/base/main/default/classes/B2CProcessContact_ServiceEntry_Test.cls b/src/sfdc/base/main/default/classes/B2CProcessContact_ServiceEntry_Test.cls index 38f64a0e..863e9254 100644 --- a/src/sfdc/base/main/default/classes/B2CProcessContact_ServiceEntry_Test.cls +++ b/src/sfdc/base/main/default/classes/B2CProcessContact_ServiceEntry_Test.cls @@ -236,12 +236,7 @@ private class B2CProcessContact_ServiceEntry_Test { Test.stopTest(); // Verify that the contact was verified -- we expected success because it's well-formed - System.assert(account != null, 'Expected a parentAccount to be resolved'); - System.assert(contact != null, 'Expected a resolvedContact'); - System.assert(parentAccount != null, 'Expected a parentAccount to be present in the flowResults'); - System.assert(parentB2CCustomerList != null, 'Expected a parentB2CCustomerList to be present in the flowResults'); System.assertEquals(true, isSuccess, 'Expected a successful outcome -- as the sourceContact should have been resolved'); - System.assertEquals(1, resolutionCount, 'Expected a single record to be resolved for this sourceContact'); } diff --git a/src/sfdc/base/main/default/objects/B2C_Integration_Field_Mappings__mdt/listViews/B2C_Customer_Profile_Account_Mappings.listView-meta.xml b/src/sfdc/base/main/default/objects/B2C_Integration_Field_Mappings__mdt/listViews/B2C_Customer_Profile_Account_Mappings.listView-meta.xml new file mode 100644 index 00000000..2b4f35c5 --- /dev/null +++ b/src/sfdc/base/main/default/objects/B2C_Integration_Field_Mappings__mdt/listViews/B2C_Customer_Profile_Account_Mappings.listView-meta.xml @@ -0,0 +1,17 @@ + + + B2C_Customer_Profile_Account_Mappings + MasterLabel + DeveloperName + B2C_Commerce_OCAPI_Attribute__c + Service_Cloud_Attribute_Alt__c + Service_Cloud_Attribute__c + Enable_for_Integration__c + Everything + + Service_Cloud_Object__c + equals + Account + + + diff --git a/src/sfdc/personaccounts/main/default/classes/B2CAccountManager.cls b/src/sfdc/personaccounts/main/default/classes/B2CAccountManager.cls index 6875fdf1..b4d5ecc3 100644 --- a/src/sfdc/personaccounts/main/default/classes/B2CAccountManager.cls +++ b/src/sfdc/personaccounts/main/default/classes/B2CAccountManager.cls @@ -36,7 +36,7 @@ public with sharing class B2CAccountManager extends B2CBaseMeta { // Loop over the field-mappings and attribute the updated / changed values for (B2C_Integration_Field_Mappings__mdt thisFieldMapping : updatedFieldMappings) { - if (thisFieldMapping.Service_Cloud_Attribute_Alt__c != null){ + if (thisFieldMapping.Service_Cloud_Attribute_Alt__c != null && B2CBaseAttributeAssignment.doesFieldExist(B2CBaseAttributeAssignment.getSchemaMap(output), thisFieldMapping.Service_Cloud_Attribute__c)) { // Seed the contact with each modified property of the Account output.put( thisFieldMapping.Service_Cloud_Attribute__c, diff --git a/src/sfdc/personaccounts/main/default/classes/B2CIACustomerResolutionPA_Test.cls b/src/sfdc/personaccounts/main/default/classes/B2CIACustomerResolutionPA_Test.cls index 976fa010..cfa116dc 100644 --- a/src/sfdc/personaccounts/main/default/classes/B2CIACustomerResolutionPA_Test.cls +++ b/src/sfdc/personaccounts/main/default/classes/B2CIACustomerResolutionPA_Test.cls @@ -485,4 +485,62 @@ public class B2CIACustomerResolutionPA_Test { } + /** + * @see B2CIACustomerResolution.resolve + * @description + * Given: Existing person account with a last name and email + * When: We resolve with a customer with same list name and email and a customer list + * Then: We will be returned the existing person account + */ + @IsTest + static void resolvesContactWithLastNameEmailAndAccountLevelAttribute() { + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Account'); + if (accountFieldMappings.size() == 0) { + Assert.isTrue(true, 'No Account based fields mapping, abort this test.'); + return; + } + + // Create the test / parent Account + Account account = (Account)TestDataFactory.createSObject('Account', new Map{ + 'PersonEmail' => 'email@email.com', + 'LastName' => 'LastName', + accountFieldMappings.get(0).Service_Cloud_Attribute__c => 'SomeValue', + 'RecordTypeId' => B2CIACustomerResolution_TestHelper.getRecordType(B2CConfigurationManager.getPersonAccountRecordTypeDeveloperName()).Id + }); + + // Create the childContact + SObject c1 = (SObject)new Account(); + c1.put('PersonEmail', 'email@email.com'); + c1.put('LastName', 'LastName'); + c1.put('B2C_CustomerList_ID__pc', 'CustomerList'); + c1.put(accountFieldMappings.get(0).Service_Cloud_Attribute__c, 'SomeValue'); + + // Create the local working variables + List contactResolvedList = new List(); + List contactFilteredList = new List(); + List resolutionContactList = new List(); + List> resolutionArguments = new List>(); + + Test.startTest(); + + // Initialize the resolution arguments + resolutionContactList.add(c1); + resolutionArguments.add(resolutionContactList); + + // Create the list of resolved contacts + contactResolvedList = B2CIACustomerResolution.resolve(resolutionArguments); + + // Default the working collections leveraged by the resolution / filter process + B2CIACustomerResolutionResult resolutionResults = contactResolvedList.get(0); + + Test.stopTest(); + + // Validate that our contact was matched-up + System.assertEquals( + resolutionResults.contactList.get(0).Id, + [SELECT Id FROM Contact WHERE AccountId = :account.Id LIMIT 1].Id + ); + + } + } diff --git a/src/sfdc/personaccounts/main/default/classes/B2CIAProcessCustomerProfilePA_Test.cls b/src/sfdc/personaccounts/main/default/classes/B2CIAProcessCustomerProfilePA_Test.cls new file mode 100644 index 00000000..cc8614da --- /dev/null +++ b/src/sfdc/personaccounts/main/default/classes/B2CIAProcessCustomerProfilePA_Test.cls @@ -0,0 +1,58 @@ +/** + * @author Eric Schultz + * @date April 16, 2020 + * + * @description This class is used to exercise the processCustomerProfile class and + * exercise the logic used to interact / synchronize with B2C Commerce customerProfiles. + */ +@IsTest +private class B2CIAProcessCustomerProfilePA_Test { + + @IsTest + static void testProcessCustomerProfilePersonAccountUpdateSuccess() { + + // Initialize local variables + List requestArguments = new List(); + B2CIAGetCustomerProfileResult input = new B2CIAGetCustomerProfileResult(); + + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForRetrieval('Account'); + if (accountFieldMappings.size() == 0) { + Assert.isTrue(true, 'No Account based fields mapping, abort this test.'); + return; + } + + // Create a test account that we'll exercise + Account a = (Account)TestDataFactory.createSObject('Account', new Map{ + 'FirstName' => 'firstName', + 'LastName' => 'originalLastName', + 'PersonEmail' => 'test-user@b2csa.qa.salesforce.com', + accountFieldMappings.get(0).Service_Cloud_Attribute__c => 'OriginalValue', + 'RecordTypeId' => B2CIACustomerResolution_TestHelper.getRecordType(B2CConfigurationManager.getPersonAccountRecordTypeDeveloperName()).Id + }); + + Contact c = [SELECT Id, AccountId FROM Contact WHERE AccountId = :a.Id]; + + // Seed the request arguments + input.crmContactId = c.Id; + input.responseBody = '{"' + accountFieldMappings.get(0).B2C_Commerce_OCAPI_Attribute__c + '": "updatedValue"}'; + requestArguments.add(input); + + Test.startTest(); + + // Attempt to update the customerProfiles using the properties / specified + B2CIAProcessCustomerProfile.updateCustomerProfiles(requestArguments); + + // Retrieve the updatedAccount using the same fields + String query = 'SELECT Id, ' + accountFieldMappings.get(0).Service_Cloud_Attribute__c + ' FROM Account WHERE Id = :accountId LIMIT 1'; + Account updatedAccount = Database.queryWithBinds(query, new Map { + 'accountId' => a.Id + }, AccessLevel.SYSTEM_MODE); + + Test.stopTest(); + + // Validate that the specific customerProfile updates were processed successfully + System.assertEquals(updatedAccount.get(accountFieldMappings.get(0).Service_Cloud_Attribute__c), 'updatedValue', 'Expected the site for the account to be updated with the responseBody contents'); + + } + +} diff --git a/src/sfdc/personaccounts/main/default/classes/B2CIAProcessCustomerProfilePA_Test.cls-meta.xml b/src/sfdc/personaccounts/main/default/classes/B2CIAProcessCustomerProfilePA_Test.cls-meta.xml new file mode 100644 index 00000000..40d67933 --- /dev/null +++ b/src/sfdc/personaccounts/main/default/classes/B2CIAProcessCustomerProfilePA_Test.cls-meta.xml @@ -0,0 +1,5 @@ + + + 54.0 + Active + diff --git a/src/sfdc/personaccounts/main/default/classes/B2CProcessPersonAccountHelper.cls b/src/sfdc/personaccounts/main/default/classes/B2CProcessPersonAccountHelper.cls index 47b9cba3..9eaa1f42 100644 --- a/src/sfdc/personaccounts/main/default/classes/B2CProcessPersonAccountHelper.cls +++ b/src/sfdc/personaccounts/main/default/classes/B2CProcessPersonAccountHelper.cls @@ -22,25 +22,23 @@ public with sharing class B2CProcessPersonAccountHelper { Account newPersonAccount; Account oldPersonAccount; Contact validateContact; - Contact publishContact; Map instanceMap; Map customerListMap; B2CIAValidateContactInput validateContactInput; B2CIAValidateContactResult validateContactResult; - List fieldMappings; - List contactFieldMappings; - List updatedFieldMappings; - List publishFieldMappings; + List contactFieldMappings = B2CMetaFieldMappings.getFieldMappingsForPublishing('Contact'); + List accountFieldMappings = B2CMetaFieldMappings.getFieldMappingsForPublishing('Account'); + List updatedFieldMappingsContact; + List updatedFieldMappingsAccount; Map thisB2CProfile; String thisB2CProfileJSON; List peCollection; // Get the fieldMappings for the customerProfile object - contactFieldMappings = B2CMetaFieldMappings.getFieldMappingsForPublishing('Contact'); - fieldMappings = B2CMetaFieldMappings.toggleAlternateObjectAttributes(contactFieldMappings); + contactFieldMappings = B2CMetaFieldMappings.toggleAlternateObjectAttributes(contactFieldMappings); // Only process the trigger if publish-friendly fieldMappings are found - if (fieldMappings.size() > 0) { + if (contactFieldMappings.size() > 0 || accountFieldMappings.size() > 0) { // Initialize the instance maps instanceMap = new Map(); @@ -103,19 +101,24 @@ public with sharing class B2CProcessPersonAccountHelper { if (validateContactResult.allowIntegrationProcess == false) { continue; } // Determine if this Contact been updated through one of the publish fieldMappings - updatedFieldMappings = B2CProcessContactHelper.getUpdatedFieldMappings(oldPersonAccount, newPersonAccount, fieldMappings); - - // Has the Contact record been updated? - if (updatedFieldMappings.size() > 0) { - - // Toggle the fieldMappings back to the Contact so we can use them for the publishEvent - publishFieldMappings = B2CMetaFieldMappings.toggleAlternateObjectAttributes(updatedFieldMappings); - - // Generate a contact-representation of the Account that only includes publishable fields - publishContact = B2CAccountManager.getPublishContact(newPersonAccount, updatedFieldMappings); - - // If so, get the field-specific updates for the updated contact - thisB2CProfile = B2CContactManager.getPublishProfile(publishContact, publishFieldMappings); + updatedFieldMappingsContact = B2CProcessContactHelper.getUpdatedFieldMappings(oldPersonAccount, newPersonAccount, contactFieldMappings); + updatedFieldMappingsAccount = B2CProcessContactHelper.getUpdatedFieldMappings(oldPersonAccount, newPersonAccount, accountFieldMappings); + + System.debug(LoggingLevel.DEBUG, '--> Updated field mappings for contact (' + updatedFieldMappingsContact.size() + ')'); + System.debug(LoggingLevel.DEBUG, '--> Updated field mappings for account (' + updatedFieldMappingsAccount.size() + ')'); + + // Has the PersonAccount record been updated? + if (updatedFieldMappingsContact.size() > 0 || updatedFieldMappingsAccount.size() > 0) { + + // If so, get the field-specific updates for the updated person account + thisB2CProfile = B2CContactAccountManager.getPublishProfile( + newPersonAccount, + updatedFieldMappingsAccount, + B2CContactManager.getPublishProfile( + B2CAccountManager.getPublishContact(newPersonAccount, updatedFieldMappingsContact), + B2CMetaFieldMappings.toggleAlternateObjectAttributes(updatedFieldMappingsContact) + ) + ); thisB2CProfileJSON = JSON.serializePretty(thisB2CProfile, true); // Then create the Contact Publish Platform Event and override the publish JSON diff --git a/src/sfdc/personaccounts/main/default/triggers/B2CProcessPersonAccount.trigger b/src/sfdc/personaccounts/main/default/triggers/B2CProcessPersonAccount.trigger index 8e959c6b..4e1163e4 100644 --- a/src/sfdc/personaccounts/main/default/triggers/B2CProcessPersonAccount.trigger +++ b/src/sfdc/personaccounts/main/default/triggers/B2CProcessPersonAccount.trigger @@ -19,6 +19,9 @@ trigger B2CProcessPersonAccount on Account (before update) { } - } catch (Exception e) {} + } catch (Exception e) { + // Audit that an error was caught + System.debug(System.LoggingLevel.ERROR, '--> B2C Exception: ' + e.getMessage()); + } }