Merges in Salesforce are really strange sometimes. You create processes around other business units and then all of a sudden something doesn’t work down the road because you forgot that Accounts and Contacts can be merged into other Accounts and Contacts. Not only that, sometimes there isn’t even a great way to detect a merge, although usually there is which I am VERY grateful for.
When companies deal with large data volume, sometimes merges cause havoc, especially if you’re dealing with distributed systems with integrations and delegated authentication systems. Now what happens when a Contact is merged into another and the Losing contact is the one that had all of the company data behind it? Woof.
We run into this problem with all of those systems mentioned above, but also reporting cubes which are stored outside of Salesforce (obviously). Our reporting team does a great job facilitating each business team with the data they need, and creating processes to keep this data correct. Unfortunately, as you can imagine, when data miraculously becomes other data with different related Ids there can be some heartburn. It’s also a huge pain that once the data is merged in Salesforce, it is effectively gone unless you want to go through the trouble of undeleting but you will never get back the old relationships. So how about searching or reporting in Salesforce on this old data?
So I tried to create an app that would monitor merges and create data on the backend whenever our business people do an Account or Contact merge. This could be extended to any object you want to build your own Merge process around as well, but this code runs for us just on Account and Contact.
The process is:
- Person identifies an Account or Contact that needs to be merged
- Person merges said Account or Contact in Salesforce using the built in merge Account or Contact functionality
- In code, we create a Merge Audit record in Salesforce
- Our BI system picks up these merges through integration and does the right thing by correcting all of the relational data used by other teams
It actually works pretty well. One thing that I want to do down the road to enhance this process is replace the Integration with a Platform Event which would be a great use case for this.
Anyway, let’s get into the code. We start off in our Account Trigger Handler which is passed data from our Account Trigger with a method called only on afterDelete which is named logMerge:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void logMerge(Account[] AccountOld){ | |
List<Merge_Audit__c> audit = new List<Merge_Audit__c>(); | |
List<Account> mergeAccounts = new List<Account>(); | |
Set<String> winnerIds = new Set<String>(); | |
Map<String,String> idMap = new Map<String,String>(); | |
Map<String,String> nameMap = new Map<String,String>(); | |
for(Account a : AccountOld){ | |
if(String.isNotBlank(a.MasterRecordId)){ | |
winnerIds.add(a.MasterRecordId); | |
mergeAccounts.add(a); | |
} | |
} | |
List<Account> accs = new List<Account>([Select Id, WG_Account_ID__c, Name from Account Where Id in :winnerIds]); | |
for(Account a : accs){ | |
idMap.put(a.Id, a.WG_Account_ID__c); | |
nameMap.put(a.Id, a.Name); | |
} | |
if(!mergeAccounts.isEmpty()){ | |
for(Account a : AccountOld){ | |
String jsonString = JSON.serializePretty(a); | |
String winnerId = idMap.get(a.MasterRecordId); | |
String loserId = a.WG_Account_ID__c; | |
Merge_Audit__c m = Utilities.createMergeAudit(a.MasterRecordId,a.Id,winnerId,loserId,'Account',jsonString); | |
m.Winner_Account__c = a.MasterRecordId; | |
m.Winning_Name__c = nameMap.get(a.MasterRecordId); | |
m.Losing_Name__c = a.Name; | |
audit.add(m); | |
} | |
} | |
if(!audit.isEmpty()){ | |
insert audit; | |
} | |
} |
There is a field on all records called MasterRecordId which is populated when a merge takes place. This is how you can determine if something was merged within a transaction, specifically here, after delete. On line 8, we check to see if each Account passed to us has a MasterRecordId. If yes, we add the Id of the master record (the winner) to a list and the losers to another list.
We then query for the related Winning Accounts to be used in our data structure that we create and make two maps for name and Id.
Last, we go through each account passed into the trigger again in the afterDelete event and create a Merge Audit record (Merge_Audit__c), assigning the right data to the record. An added bonus here, I wanted to be able to see all of the related losing Account data after the data was deleted, so we create a JSON object out of the losing Account and dump the JSON into a field. Related Utility below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static Merge_Audit__c createMergeAudit(String winner, String loser, String winnerWGID, String loserWGID, String mergeType, String json){ | |
Merge_Audit__c m = new Merge_Audit__c(Winning_Id__c = winner, | |
Losing_Id__c = loser, | |
Winning_WG_Id__c = winnerWGID, | |
Losing_WG_Id__c = loserWGID, | |
Type__c = mergeType, | |
Losing_Record_Detail__c = json); | |
return m; | |
} |
The schema for our Merge Audit object here:
The same thing can be replicated on Contact or any other object. We also link the Account or Contact directly on the Merge Audit and have the related list of the Merge Audit on the Account and Contact layouts. If you ever want to go back in time and see all of the merges that have made a certain Account or Contact what it is today, you will have the ability to see all merges that were directly merged into the current record, or previous merges that were then merged into the current record in a crazy unlimited hierarchy. If you apply field tracking, you can then see the full history of merges on the Merge Audit, which is neat.
After you have written this small amount of code, you end up with something like this:

One other fun fact about this, is it is reportable and can trigger other automation from this which I really like. Bonus points, you can also search for data and old merges will be returned in results which is especially helpful if there isn’t any current data that matches what you’re looking for. You can then put your detective hat on and follow the trail to figure out what happened.