Examples & Use Cases

Merge plugin

Below are example implementations of the Duplicate Check Merge Plugin, each demonstrating a different use case.

Basic Example Code

global class DefaultMerge implements dupcheck.dc3Plugin.InterfaceMerge {

    global void beforeMerge(String objectPrefix, Sobject masterRecord, List<sobject> mergedRecordList) {
        // YOUR CUSTOM CODE
        System.debug('beforeMerge called for object prefix: ' + objectPrefix);
        System.debug('Master record: ' + masterRecord);
        System.debug('Records to be merged: ' + mergedRecordList);
        return;
    }

    global void mergeFailed(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds, dupcheck.dc3Exception.MergeException exceptionData) {
        // YOUR CUSTOM CODE
        System.debug('mergeFailed called for object prefix: ' + objectPrefix);
        System.debug('Master record: ' + masterRecord);
        System.debug('Merged Record Ids: ' + mergedRecordIds);
        System.debug('Exception Data: ' + exceptionData.getMessage());
        return;
    }

    global void afterMerge(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds) {
        // YOUR CUSTOM CODE
        System.debug('afterMerge called for object prefix: ' + objectPrefix);
        System.debug('Master record: ' + masterRecord);
        System.debug('Merged Record Ids: ' + mergedRecordIds);
        return;
    }
}

Example 1: Block a merge based on certain conditions

This example stops a merge operation if any of the involved Lead records (master or loser) has the Stop_Merge__c checkbox set to true. In this example, the plugin first gathers all involved record IDs, then queries all those Lead records to check the value of the Stop_Merge__c field. If any record has this field set to true, a custom exception is thrown to cancel the merge.

global class ConditionalMergeBlockPlugin implements dupcheck.dc3Plugin.InterfaceMerge {

    global class MergeStopException extends Exception {}

    global void beforeMerge(String objectPrefix, Sobject masterRecord, List<sobject> mergedRecordList) {
        // Only process for Leads (object prefix '00Q')
        if (objectPrefix.equalsIgnoreCase('00Q')) {
            // Gather IDs of the master record and all records to be merged
            Set<Id> allIds = new Set<Id>{ masterRecord.Id };
            for (Sobject leadObj : mergedRecordList) {
                allIds.add(leadObj.Id);
            }
            
            // Query all Lead records involved to get the latest value for Stop_Merge__c
            List<Lead> allLeads = [SELECT Id, Stop_Merge__c FROM Lead WHERE Id IN :allIds];
            
            // If any record has Stop_Merge__c set to true, cancel the merge by throwing an exception
            for (Lead l : allLeads) {
                if (l.Stop_Merge__c == true) {
                    throw new MergeStopException('Merge stopped: One or more records have Stop_Merge__c set to true.');
                }
            }
        }
        return;
    }
    
    global void mergeFailed(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds, dupcheck.dc3Exception.MergeException exceptionData) 
        return;
    }
    
    global void afterMerge(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds) {
        return;
    }
}

Example 2: Email notification if a merge succeeds or fails

This use case sends an email notification when a merge operation either succeeds or fails. The email is sent to the owner of the (intended) master Lead record.

global class EmailNotificationMergePlugin implements dupcheck.dc3Plugin.InterfaceMerge {

    global void beforeMerge(String objectPrefix, Sobject masterRecord, List<sobject> mergedRecordList) {
        return;
    }
    
    // When the merge fails, send an email with the failure details.
    global void mergeFailed(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds, dupcheck.dc3Exception.MergeException exceptionData) {
        if (objectPrefix.equalsIgnoreCase('00Q')) {
            // Query the master Lead record to retrieve the owner's email.
            List<Lead> leadList = [SELECT Id, Name, Owner.Email FROM Lead WHERE Id = :masterRecord.Id LIMIT 1];
            if (!leadList.isEmpty() && leadList[0].Owner.Email != null) {
                String subject = 'Merge Failure for Lead ' + leadList[0].Name;
                String body = 'Merge failed for master record Id: ' + masterRecord.Id + '\n';
                body += 'Merged Record Ids: ' + JSON.serialize(mergedRecordIds) + '\n';
                body += 'Exception Message: ' + exceptionData.getMessage();
                
                Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
                email.setToAddresses(new List<String>{ leadList[0].Owner.Email });
                email.setSubject(subject);
                email.setPlainTextBody(body);
                Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ email });
            }
        }
        return;
    }
    
    // After a successful merge, send an email notification to the owner.
    global void afterMerge(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds) {
        if (objectPrefix.equalsIgnoreCase('00Q')) {
            // Query the master Lead record to retrieve the owner's email.
            List<Lead> leadList = [SELECT Id, Name, Owner.Email FROM Lead WHERE Id = :masterRecord.Id LIMIT 1];
            if (!leadList.isEmpty() && leadList[0].Owner.Email != null) {
                String subject = 'Merge Completed for Lead ' + leadList[0].Name;
                String body = 'The merge operation successfully merged ' + mergedRecordIds.size() + ' records into Lead ' + leadList[0].Name + '.';
                
                Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
                email.setToAddresses(new List<String>{ leadList[0].Owner.Email });
                email.setSubject(subject);
                email.setPlainTextBody(body);
                Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ email });
            }
        }
        return;
    }
}

Example 3: Do not merge, but create Account Hierarchy

This use case demonstrates how to override the normal merge behavior by creating an account hierarchy instead. In this scenario, rather than merging duplicate Account records, the master record becomes the parent and the loser records become its children. The plugin then throws a NoProcessException to cancel the standard merge process.

global class HierarchyCreationMergePlugin implements dupcheck.dc3Plugin.InterfaceMerge {

    global void beforeMerge(String objectPrefix, Sobject masterRecord, List<sobject> mergedRecordList) {
        // Only process for Account records (object prefix '001')
        if (objectPrefix.equalsIgnoreCase('001')) {
            // Cast masterRecord to Account to serve as the parent account.
            Account parentAccount = (Account) masterRecord;
            List<Account> childAccounts = new List<Account>();
            
            // Iterate through the loser records and update each to set its ParentId to the master record's Id.
            for (Sobject acc : mergedRecordList) {
                Account childAccount = (Account) acc;
                childAccount.ParentId = parentAccount.Id;
                childAccounts.add(childAccount);
            }
            
            try {
                // Update the child accounts to create the hierarchy.
                update childAccounts;
                System.debug('Account hierarchy created. Parent Account: ' + parentAccount.Id);
            } catch (DmlException e) {
                System.debug(LoggingLevel.ERROR, 'Error updating child accounts: ' + e.getMessage());
            }
            
            // Instead of executing the standard merge process, throw a NoProcessException.
            throw new dupcheck.dc3Exception.NoProcessException('Account hierarchy created instead of performing merge.');
        }
        return;
    }
    
    global void mergeFailed(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds, dupcheck.dc3Exception.MergeException exceptionData) {
        return;
    }
    
    global void afterMerge(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds) {
        return;
    }
}

Example 4: Notify (loser) record owners of merge activity

This example demonstrates how to notify each original owner of the “loser” records when a merge completes. It captures the old owner and record name in beforeMerge, then uses that data in afterMerge to send a native Salesforce notification (the standard bell icon) listing the merged record names and identifying the new owner. This specific example if tailored for the Lead object.

Required steps in Salesforce setup

  • Create a Custom Notification with the API Name MergeNotification at Setup -> Notification Builder -> Custom Notifications.
  • Enable Users to receive notifications.
global class MultiOwnerNotificationMergePlugin implements dupcheck.dc3Plugin.InterfaceMerge {
    // Store loser record data during beforeMerge
    private static Map<Id, String> oldOwnerMap = new Map<Id, String>();
    private static Map<Id, String> oldNameMap = new Map<Id, String>();

    global void beforeMerge(String objectPrefix, Sobject masterRecord, List<Sobject> mergedRecordList) {
        // Only process for Leads
        if (objectPrefix.equalsIgnoreCase('00Q')) {
            for (Sobject s : mergedRecordList) {
                Lead loser = (Lead) s;
                oldOwnerMap.put(loser.Id, String.valueOf(loser.OwnerId));
                oldNameMap.put(loser.Id, loser.Name);
            }
        }
    }
    
    global void mergeFailed(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds, dupcheck.dc3Exception.MergeException exceptionData) {
        return;
    }
    
    // After a successful merge, re-query the master record and build notifications for owners of the loser records
    global void afterMerge(String objectPrefix, Sobject masterRecord, Set<Id> mergedRecordIds) {
        if (!objectPrefix.equalsIgnoreCase('00Q')) {
            return;
        }

        // Query the master lead to get the real OwnerId and Name
        Lead queriedMaster = [SELECT Id, Name, OwnerId FROM Lead WHERE Id = :masterRecord.Id LIMIT 1];
        
        // Query the new owner’s user record so we can display the new owner's name
        User masterOwner = [SELECT Id, Name FROM User WHERE Id = :queriedMaster.OwnerId LIMIT 1];

        // Build a map of oldOwnerId -> list of record names
        Map<Id, List<String>> ownerToLoserNames = new Map<Id, List<String>>();
        for (Id loserId : mergedRecordIds) {
            if (!oldOwnerMap.containsKey(loserId)) {
                continue;
            }
            Id oldOwnerId = Id.valueOf(oldOwnerMap.get(loserId));
            // Skip if the old owner is the same as the new master lead's owner
            if (oldOwnerId == queriedMaster.OwnerId) {
                continue;
            }
            if (!ownerToLoserNames.containsKey(oldOwnerId)) {
                ownerToLoserNames.put(oldOwnerId, new List<String>());
            }
            String recordName = oldNameMap.containsKey(loserId) ? oldNameMap.get(loserId) : 'Unknown Record';
            ownerToLoserNames.get(oldOwnerId).add(recordName);
        }

        // If there are no owners who lost their records, do nothing
        if (ownerToLoserNames.isEmpty()) {
            return;
        }

        // Query the custom notification type with DeveloperName = "MergeNotification"
        CustomNotificationType cnt = [SELECT Id FROM CustomNotificationType WHERE DeveloperName = 'MergeNotification' LIMIT 1];

        // For each old owner, send one notification
        for (Id oldOwnerId : ownerToLoserNames.keySet()) {
            List<String> recordNames = ownerToLoserNames.get(oldOwnerId);
            String messageBody = 'The following record(s) that you owned were merged into Lead '
                + queriedMaster.Name
                + '. The new owner is '
                + masterOwner.Name
                + '. You are no longer the owner of: '
                + String.join(recordNames, ', ')
                + '.';

            Messaging.CustomNotification notification = new Messaging.CustomNotification();
            notification.setNotificationTypeId(cnt.Id);
            notification.setTitle('Merge Notification for Lead ' + queriedMaster.Name);
            notification.setBody(messageBody);
            notification.setTargetId(queriedMaster.Id);
            notification.setSenderId(UserInfo.getUserId());
            
            // The send method requires a set of user IDs in String form
            notification.send(new Set<String>{ String.valueOf(oldOwnerId) });
        }
    }
}

Example 5: Consolidate Account Contact Relationship Roles

This merge plugin is designed for use during contact merge events. It consolidates the “Roles” field on Account Contact Relationship (ACR) records for contacts that belong to the same account. The plugin groups all related ACR records by AccountId, combines their role values into a unified semicolon-delimited string, removes duplicate role entries, and updates every ACR record with the consolidated role set. This ensures that after a merge, the account’s relationship roles are represented consistently and without redundancy.

global class MergePluginRoles implements dupcheck.dc3Plugin.InterfaceMerge {   
    
       global void beforeMerge(String objectPrefix, SObject masterRecord, List<SObject> mergedRecordList) {
        // Process only for Contact merge events (Contacts have the prefix '003').
        if (objectPrefix.equals('003')) {
            
            // Map to group AccountContactRelation records by AccountId.
            Map<Id, List<AccountContactRelation>> acrMap = new Map<Id, List<AccountContactRelation>>();
            // List to store ACR records that need to be updated.
            List<AccountContactRelation> acrToUpdate = new List<AccountContactRelation>();

            // Query all AccountContactRelation records associated with the master and merged contacts.
            for (AccountContactRelation acr : [
                SELECT AccountId, Roles 
                FROM AccountContactRelation 
                WHERE ContactId IN :mergedRecordList OR ContactId = :masterRecord.Id
            ]) {
                if (!acrMap.containsKey(acr.AccountId)) {
                    acrMap.put(acr.AccountId, new List<AccountContactRelation>{ acr });
                } else {
                    acrMap.get(acr.AccountId).add(acr);
                }
            }
            
            // Loop through each set of ACR records grouped by AccountId.
            for (Id accountId : acrMap.keySet()) {
                // Initialize an empty string to hold consolidated role values.
                String consolidatedRoles = '';
                
                // Iterate over each ACR record for the account.
                for (AccountContactRelation acrRec : acrMap.get(accountId)) {
                    // Only process non-blank roles.
                    if (String.isNotBlank(acrRec.Roles)) {
                        // If consolidatedRoles is empty, start with the current value.
                        if (String.isBlank(consolidatedRoles)) {
                            consolidatedRoles = acrRec.Roles;
                        } else {
                            // Append roles using a semicolon separator.
                            consolidatedRoles += ';' + acrRec.Roles;
                        }
                    }
                }
                
                // Remove duplicate role entries by splitting, deduplicating, and rejoining.
                List<String> roleList = consolidatedRoles.split(';');
                Set<String> uniqueRoles = new Set<String>(roleList);
                consolidatedRoles = String.join(new List<String>(uniqueRoles), ';');
                                
                // Update every ACR record in this group with the consolidated roles.
                for (AccountContactRelation acrRec : acrMap.get(accountId)) {
                    acrRec.Roles = consolidatedRoles;
                    acrToUpdate.add(acrRec);
                }
            }

            // Commit the updates to the AccountContactRelation records.
            update acrToUpdate;
        }
    }

    /**
     * Called if the merge operation fails.
     */
    global void mergeFailed(String objectPrefix, SObject masterRecord, Set<Id> mergedRecordsIds, dupcheck.dc3Exception.MergeException exceptionData) {
        // Custom logic for handling merge failures can be added here.
        return;
    }

    /**
     * Called after a successful merge.
     */
    global void afterMerge(String objectPrefix, SObject masterRecord, Set<Id> mergedRecordIds) {
        // Custom post-merge logic can be added here.
        return;
    }
}

Example 6: Override Unfriendly Merge Errors

This merge plugin demonstrates how to override unfriendly or technical Salesforce error messages during a merge operation. In this example, where a merge fails due to missing required fields (a Mobile Phone field for Contact records), the plugin intercepts the error, then throws a new, user-friendly exception. This approach can be adapted to provide clear guidance for any error condition encountered during merge operations.

global class CustomMergeErrorMobilePhone implements dupcheck.dc3Plugin.InterfaceMerge {

    // Called before the merge operation. No custom logic is required here.
    global void beforeMerge(String objectPrefix, SObject masterRecord, List<SObject> mergedRecordList) {
        System.debug('Before Merge - Master Record: ' + masterRecord);
        return;
    }    

    global void mergeFailed(String objectPrefix, SObject masterRecord, Set<Id> mergedRecordIds, dupcheck.dc3Exception.MergeException exceptionData) {
        // Process only Contact records (prefix '003').
        if (objectPrefix.equals('003')) {
            // Check if the error message indicates that Mobile Phone is required.
            // This condition assumes the default error contains specific text.
            if (exceptionData.getMessage().endsWith('with id ' + masterRecord.Id + '; first error: FIELD_CUSTOM_VALIDATION_EXCEPTION, Mobile Phone is required: [MobilePhone]')
                && exceptionData.getMessage().startsWith('Merge Failed: Merge failed. First exception on row')) {
                // Override the default error with a more user-friendly message.
                throw new dupcheck.dc3Exception.MergeException('Merge failed: A valid Mobile Phone Number is required for Contacts. Please update the record and try again.');
            }
        }
        return;
    }

    // Called after a successful merge. No additional logic is implemented in this example.
    global void afterMerge(String objectPrefix, SObject masterRecord, Set<Id> mergedRecordIds) {
        return;
    }
}