Skip to content

Instantly share code, notes, and snippets.

@douglascayers
Last active March 16, 2022 06:35
Show Gist options
  • Save douglascayers/9df97af91ac6822758a8 to your computer and use it in GitHub Desktop.
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
/**
* 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;
}
}
/**
* 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 );
}
}
}
}
/**
* 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;
}
}
@chrishigh
Copy link

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.

@rovalm
Copy link

rovalm commented Feb 12, 2019

@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