Last active
March 16, 2022 06:35
-
-
Save douglascayers/9df97af91ac6822758a8 to your computer and use it in GitHub Desktop.
Devising way to use Salesforce process builder and flow to automate merging duplicate records
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
/** | |
* Developed by Doug Ayers (douglascayers.com) | |
*/ | |
public with sharing class DuplicateRecordUtils { | |
public static Set<ID> getDuplicateRecordIds( ID recordId ) { | |
Set<ID> duplicateRecordIds = new Set<ID>(); | |
// Potential duplicate records are grouped together into 'duplicate record sets'. | |
// This query looks for dupe sets that the record id argument is associated to. | |
// The sub-query finds all potential duplicate records within the matched sets | |
// and explicitly excludes the record id (itself) from the sub-query results, | |
// which is just a convenient way to not include the record id in the returned result. | |
List<DuplicateRecordSet> duplicateSets = new List<DuplicateRecordSet>([ | |
SELECT | |
id, name, | |
( SELECT recordId FROM DuplicateRecordItems WHERE recordId != :recordId ) | |
FROM | |
DuplicateRecordSet | |
WHERE | |
id IN ( | |
SELECT duplicateRecordSetId FROM DuplicateRecordItem WHERE recordId = :recordId | |
) | |
]); | |
// Add all the potential duplicate record ids to a common set. | |
for ( DuplicateRecordSet duplicateSet : duplicateSets ) { | |
if ( duplicateSet.duplicateRecordItems != null ) { | |
for ( DuplicateRecordItem duplicateRecord : duplicateSet.duplicateRecordItems ) { | |
duplicateRecordIds.add( duplicateRecord.recordId ); | |
} | |
} | |
} | |
return duplicateRecordIds; | |
} | |
} |
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
/** | |
* Developed by Doug Ayers (douglascayers.com) | |
*/ | |
public class MergeDuplicateAccountsAction { | |
/* | |
* Given an accountId, assume it will be the master record id. | |
* Query for any known duplicate matches. | |
* For each match, merge into this master account. | |
*/ | |
@InvocableMethod( | |
label = 'Merge Duplicate Accounts' | |
description = 'Merges records identified by your duplicate matching rules.' | |
) | |
public static void execute( List<ID> accountIds ) { | |
for ( ID accountId : accountIds ) { | |
Set<ID> duplicateRecordIds = DuplicateRecordUtils.getDuplicateRecordIds( accountId ); | |
for ( ID duplicateRecordId : duplicateRecordIds ) { | |
// Specifying master record as a 'new' account instance that is the id only | |
// because when passing in the actual account loop reference then get merge | |
// errors that we're trying to overwrite system fields like CreateDate or LastModifiedDate. | |
// And since the merge operation requires the master record be an sobject reference, | |
// this seemed like the simplest workaround to implement. | |
Database.merge( new Account( id = accountId ), duplicateRecordId ); | |
} | |
} | |
} | |
} |
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
/** | |
* Developed by Doug Ayers (douglascayers.com) | |
*/ | |
@isTest | |
private class MergeDuplicateAccountsActionTest { | |
@isTest | |
static void test_merge() { | |
Account acct1 = new Account( | |
name = 'Test Account', | |
billingStreet = '1 Market Street', | |
billingCity = 'San Francisco', | |
billingState = 'CA', | |
billingPostalCode = '12345', | |
billingCountry = 'USA' | |
); | |
Account acct2 = acct1.clone(); | |
List<Account> accounts = new List<Account>{ acct1, acct2 }; | |
Database.DMLOptions dmo = new Database.DMLOptions(); | |
dmo.DuplicateRuleHeader.allowSave = true; | |
Database.insert( accounts, dmo ); | |
System.assertEquals( 1, [ SELECT count() FROM Account WHERE id = :acct1.id ] ); | |
System.assertEquals( 1, [ SELECT count() FROM Account WHERE id = :acct2.id ] ); | |
// if no duplicate record sets have been created yet | |
// then let's manually insert them so we can test the merge | |
Integer dupeCount = [ SELECT count() FROM DuplicateRecordItem WHERE recordId IN :accounts ]; | |
if ( dupeCount == 0 ) { | |
insertDuplicateRecordSet( accounts ); | |
} | |
Test.startTest(); | |
MergeDuplicateAccountsAction.execute( new List<ID>{ acct1.id } ); | |
Test.stopTest(); | |
System.assertEquals( 1, [ SELECT count() FROM Account WHERE id = :acct1.id ] ); | |
System.assertEquals( 0, [ SELECT count() FROM Account WHERE id = :acct2.id ] ); | |
} | |
private static void insertDuplicateRecordSet( List<Account> accounts ) { | |
// you need at least one active duplicate rule | |
DuplicateRule rule = [ SELECT id FROM DuplicateRule WHERE sobjectType = 'Account' AND isActive = true LIMIT 1 ]; | |
DuplicateRecordSet drs = new DuplicateRecordSet( | |
duplicateRuleId = rule.id | |
); | |
insert drs; | |
List<DuplicateRecordItem> items = new List<DuplicateRecordItem>(); | |
for ( Account acct : accounts ) { | |
items.add( new DuplicateRecordItem( | |
duplicateRecordSetId = drs.id, | |
recordId = acct.id | |
)); | |
} | |
insert items; | |
} | |
} |
@douglascayers I am working on exactly same requirement.
In our org, accounts gets created from web.
We are dealing with duplicates, where a client will submit a form online, and if this client is already in the system, it creates a duplicate.
I have a duplicate rule active, where the matching rule is the standard account matching rule. (Account Name, Shipping Post Code, City)
I have a 'potential duplicate component' that shows the potential duplicates.
However instead of manually merging duplicates I want to automatically merge duplicates where primary account is the old account that is already in the system.
I can see your solution working for me, but i don't know how to create a flow to call this class.
Can you please advise ?
Thanks
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
So I don't know what I'm reading here (especially since Process Builder is almost entirely a point & click GUI) but... did it work? I have a situation where a child object record is automatically created for EVERY new and existing account. Which is fine, the problem comes in with all the accounts we have to merge - every account should only have ONE of these children, but post-merge it ends up with two and I have to do a secondary merge job just for the child object. A process builder automation upon merging of parent accounts would be fantastic.