Dhrumit Thakkar

Find Potential Duplicate and Merge Records: Lightning Datatable

Blog Image

Q : Why Find Potential Duplicate Component ? 

A : We will be creating this Find Potential Duplicate Component for Lead object.

1. On Record click it will display all potential duplicate in Lightning Datatable. Lightning Datatable position will depend on where we are positioning lightning component on page.


2. Selecting multiple duplicate records using checkbox we can merge records and keep only master record. Salesforce standard functionality allows you to merge only up to 3 records at a time. Using custom lightning component you can merge as many duplicate records you want. 


3. All merged records except Master Record will get deleted from Lead Object and we move all records including Master Record as a back up on another custom 'LeadBackup object'.
Note: You will be creating 'LeadBackup object' in later step in this blog.

4.This is drag and drop component customized for Lead object. You can place it anywhere on lightning flexi page on record. 

Also, you can reuse this for any other objects by just slightly changing some fields and object name in code.


Q : What SalesForce existing potential duplicate component missing out on ?
A: Existing potential duplicate component allows you to merge only up to 3 records. 

Once you enable potential duplicate component for an object like lead or Account or Contact. First we need to create duplicate rule. When we create duplicate rule we need to select matching rule. 

1. We can select existing Mapping rule.
2. We can create new Matching Rule and select newly created Matching rule. 
3. We can select up to 3 Matching rule.

Very Important thing to notice: 

When we create new matching rule : 
step 1 is to select an object. 
step 2 is to define matching criteria.

Now we have some limitation here. 

There is no way that we can define cross field mapping in matching criteria. 

For an Example:

We want to create matching rule on to find potential duplicate on lead object.  We have fields call Phone and MobilePhone on Lead object.
We want to consider duplicate lead if value of Phone field is matching with value of MobilePhone field of another lead. So basically we want cross field mapping in order to compare value of Phone field  to value of Phone or MobilePhone field of another Lead.

Salesforce doesn't provide cross field mapping in matching rule.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
To achieve this we will create customize solution using aura component and will add it to Lightning Flexi Page on Lead Object.
We will create one lightning component and one apex class. 

Before we jump into the code to copy and paste first we complete below steps :)

1. Make sure we have standard Phone, MobilePhone and Email fields are  already exists on Lead Object. 
2. Create custom Email field on Lead Object
    - Label: Alt Email
    - API: Alt_Email__c
3. Create Lead Backup Object
3. Create LeadBackUP Object with following field
    - Label: Name, Phone (Phone), MobilePhone (Phone),  Email,  Alt Email, LeadID, Master Lead, Master Lead ID, Master Lead URL
    - API: LastName, FirstName, Phone__c, MobilePhone__c, Email__c, Alt_Email__c, LeadID__c, MasterLead__c, MasterLead_ID__c, MasterLead_URL__c

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

1. FindDuplicateAC.apxc

public with sharing class FindDuplicateAC {
    
    /* when you click on one lead record getLeadInfo method will get current lead recordID and will find 
     * all matching leads by comparing Phone, MobilePhone, Email and AlterEmail with all other leads and 
     * getLeadInfo method will return all mathcing lead as duplicateLeadList to FindDuplicate lightning component
     */ 
    
    @AuraEnabled
    public static List<Lead> getLeadInfo(Id leadRecordID) {
        // All variable declaration in this method 
        List <Lead> duplicateLeadList;
        List<String> leadPhone = new List<String>();
        List<String> leadEmail = new List<String>();
        // Get lead details: Phone, MobilePhone, Email, Alt_Email__c from lead recordID
        Lead leadList = [SELECT phone, MobilePhone, Email, Alt_Email__c FROM Lead WHERE Id = :leadRecordID];
        
        /* Adding PhoneNumber and Mobile-PhoneNumber to leadPhone list and Email, AltEmail to leadEmail list
         * 1. Add phone to leadPhone list
         * 2. Clean Phone and add it again to leadPhone list
         * 3. Add MobilePhone to leadPhone list
         * 4. Clean MobilePhone and add it again to leadPhone list
         * 5. Add Email to leadEmail list
         * 6. Add AltEmail to leadEmail list
        */
        
        if((String.isNotBlank(leadList.Phone)) && leadList.Phone != null && leadList.Phone != ''){
            leadPhone.add(leadList.Phone); // step 1
            leadPhone.add(leadList.Phone.replaceAll('\\D','')); // step 2
        }
        if((String.isNotBlank(leadList.MobilePhone)) && leadList.MobilePhone != null && leadList.MobilePhone != ''){
            leadPhone.add(leadList.MobilePhone); // step 3
            leadPhone.add(leadList.MobilePhone.replaceAll('\\D','')); // step 4
        }
        if((String.isNotBlank(leadList.Email)) && leadList.Email != null && leadList.Email != ''){
            leadEmail.add(leadList.Email); // step 5
        }
        if((String.isNotBlank(leadList.Alt_Email__c)) && leadList.Alt_Email__c != null && leadList.Alt_Email__c != ''){
            leadEmail.add(leadList.Alt_Email__c); // step 6
        }
        
        System.debug('Hi I am leadPhone :' + leadPhone);
        System.debug('Hi I am leadEmail :' + leadEmail);
        
        // getting all duplicate leads based on matching phone, MobilePhone, Email and Alternate Email
        duplicateLeadList = [Select Id, Name, phone, MobilePhone, Email, Alt_Email__c, convertedbusiness__c, 
                             convertedbusiness__r.Name, createddate, LeadOwnerName__c, Status from Lead where 
                             (phone IN :leadPhone) OR 
                             (MobilePhone IN :leadPhone) OR 
                             (Phone_Custom__c IN :leadPhone) OR 
                             (MobilePhone_Custom__c IN :leadPhone) OR 
                             (Email IN :leadEmail) OR 
                             (Alt_Email__c IN :leadEmail)];  
        
        System.debug('All DuplicateLeads : ' +duplicateLeadList); 
        return duplicateLeadList;        
    }
    
    /* getMasterRecordEdit method will receive a map(Key,value) request from component with one Master Record which 
     * has Lead Id as a Key and 'True' string as a value in map.
     * This method delete all leads matching to Master Lead from lead object and move it to another object call LeadBackup
     */
    
    @AuraEnabled
    public static Boolean getMasterRecordEdit(Map<Id, String> myMap){
        System.debug('Hi I am myMap: ' + myMap);
        Set<Id> keys = myMap.keySet();           // getting all Id from myMap to 'keys' Set
        List < Lead> leadsCollection = new List < Lead> (); 
        List < Lead_Backup__c> leadsToMove = new List < Lead_Backup__c> ();
        List < Lead> leadsToDelete = new List < Lead> ();
        String masterLeadID;
        
        System.debug('Hi I am all keys from myMap : ' + keys);
        /* following for loop is created to get all leads one by one from database based on collected keys above 
         * 1. Add lead to leadsCollection to use it in later part of another for loop.
         * 2. If lead is not Master lead add it to leadsToDelete list.
         * 3. Else get Id of that lead and store it in masterLeadID.
         */
        for(Id key: keys){
            Lead myMapLeads = [Select Id, LastName, FirstName, Phone, MobilePhone, Email, Alt_email__c from Lead where Id =: key];
            leadsCollection.add(myMapLeads); // step 1
            if(myMap.get(myMapLeads.Id) != 'True'){
                masterLeadID = myMapLeads.Id;
                leadsToDelete.add(myMapLeads);  // step 2
            }
            else{ 
                masterLeadID = myMapLeads.Id; // step 3
            }
        }
        /* following for loop is created to store all matcing leads that we collected in leadsCollection 
         * list including master lead in lead_Backup__c object
         * 1. Storing masterleadID to MasterLead_ID__c field on Lead_Backup__c object
         * 2. Storing 'TRUE' or 'FALSE' string value on MasterLead__c field on Lead_Backup__c object
         * 3. Storing reference URL to MasterLead_URL__c field on Lead_Backup__c object
         * 4. Storing actual lead Id to LeadID__c field on Lead_Backup__c object
         * 5. Storing lead name to Name field on Lead_Backup__c object
         * 6. Storing Phone to Phone__c field on Lead_Backup__c object
         * 7. Storing MobilePhone to MobilePhone__c field on Lead_Backup__c object
         * 8. Storing Email to Email__c field on Lead_Backup__c object
         * 9. Storing Alt_Email__c to Alt_Email__c field on Lead_Backup__c object
         * 10. Move all leads to Lead_Backup__c object 
         * 11. Delete all leads except Master Lead from Lead object 
	 */
        for(Lead l : leadsCollection){
            Lead_Backup__c leadBackUP = new Lead_Backup__c();
            leadBackUP.MasterLead_ID__c = masterLeadID; // step 1
            leadBackUP.MasterLead__c = myMap.get(l.Id); // step 2
            if(String.isNotBlank(masterLeadID)){
                leadBackUp.MasterLead_URL__c = '/' + masterLeadID; // step 3
            }
            leadBackUP.LeadID__c = String.valueOf(l.Id); // step 4
            String leadBackUPName;
            if((String.isNotBlank(l.LastName)) && l.LastName != null && l.LastName != ''){
                leadBackUPName = l.LastName;
            }
            if((String.isNotBlank(l.firstName)) && l.firstName != null && l.firstName != ''){
                leadBackUPName = leadBackUPName + ' ' + l.firstName;
            }
            leadBackUP.Name = leadBackUPName; // step 5
            
            if((String.isNotBlank(l.Phone)) && l.Phone != null && l.Phone != ''){
                leadBackUP.Phone__c = l.Phone; // step 6
            }
            if((String.isNotBlank(l.MobilePhone)) && l.MobilePhone != null && l.MobilePhone != ''){
                leadBackUP.MobilePhone__c = l.MobilePhone; // step 7
            }
            if((String.isNotBlank(l.Email)) && l.Email != null && l.Email != ''){
                leadBackUP.Email__c = l.Email; // step 8
            }
            if((String.isNotBlank(l.Alt_Email__c)) && l.Alt_Email__c != null && l.Alt_Email__c != ''){ 
                leadBackUP.Alt_Email__c = l.Alt_Email__c; // step 9
            }
            leadsToMove.add(leadBackUP);
        }
        insert leadsToMove; // step 10
        delete leadsToDelete; // step 11
        return true;
    }
    
    /* updateLeadRecord method will take updated value from lightning datatable record that you decide to update and will update in database.
     * Also will return true to lightning controller and lightning controller will set updated values to component attribute which will
     * display updated values back to lightning datatable on UI */     
    @AuraEnabled
    public static Boolean updateLeadRecord(List<Lead> leadDraftRecord) {
        System.debug('Hi I am leadDraftRecord : ' + leadDraftRecord);
        update leadDraftRecord;
        return true;
    }     
}


2. FindDuplicate.cmp

<aura:component implements="force:lightningQuickActionWithoutHeader,force:hasRecordId,flexipage:availableForAllPageTypes" 
                controller="FindDuplicateAC">
    
    
    <!-- attribute lead with Object datatype is all lead records in lightning:datatable -->
    <aura:attribute name="lead" type="Object"/>
    
    <!-- This will be call doInit method on page load -->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    
    <!-- attribute columns with List datatype is all column lable in lightning:datatable -->
    <aura:attribute name="columns" type="List"/>
    
    <!-- This will store number of Rows selected in lightning:datatable --> 
    <aura:attribute name="selectedRowsCount" type="Integer" default="0"/>
    <!--  <aura:attribute name="maxRowSelection" type="Integer" default="5"/> -->
    
    <!-- attibute masterRec will store master record Id and will identify as a Master Record on lightning controller and Apex controller side. 
   Also, usefull in mergeRecords 	
    -->
    <aura:attribute name="masterRecId" type="integer"/>
    
    <!-- draftValues attribute collect all updated values in Object datataype from lightning:datatable. 
      Note: This doesn't send it to controller until you click on save 
    --> 
    <aura:attribute name="draftValues" type="Object" default="[]"/>
    
    <!-- errors attribute receives and stores all erros coming from lightning:datatable -->
    <aura:attribute name="errors" type="Object" default="[]"/>
    
    <!-- set attribute funCallDecision to True if request is coming from onDraftSave function from helper controller. 
      1. We are sharing finishOperation and showToast fucntion between two other function: onDraftSave and mergeRecords 
      2. onDraftSave: This function reload page after Save or Merge button click. 
      - Reloading page after clicking Save button which is basically 'onDraftSave function' we need to redirect to current recordId. 
      - Reloading page after clicking Merge button which is basically 'mergeRecord fuction' we need to redirect to master recordId 
        (Receiving from Apex)
    --> 
    <aura:attribute name="funCallDecision" type="Boolean" default="false"/>
    <div class="slds-box slds-theme--default">
        <div class="slds-page-header__title slds-truncate">
            Potential Duplicates
        </div>     
        <div class="slds-box">
            <h1>Selected Rows: {! v.selectedRowsCount }</h1>
            <lightning:datatable aura:id = "selectedRows"
                                 columns="{! v.columns }"
                                 data="{! v.lead }"
                                 keyField="Id"
                                 errors="{! v.errors }"
                                 draftValues="{! v.draftValues }"
                                 maxRowSelection="{! v.maxRowSelection }"
                                 onsave="{! c.onSave}"
                                 />
        </div>
        <div class="slds-box-width">
       
        <ui:inputText label="Enter Master Record ID:" value="{!v.masterRecId}" updateOn="keyup" required="true" maxlength="18"/> <br/>
        </div>
        <lightning:button label="Merge Leads" 
                          class="slds-m-top--medium"
                          variant="brand"
                          onclick="{!c.selectedRecords}"
                          />
    </div>
</aura:component>


3. FindDuplicateController.js

({
    doInit : function(cmp, event, helper) {
        helper.getDuplicateLeads(cmp);     // calling getDuplicateLeads method in                
    },
    
    updateSelectedText: function (cmp, event) {
        var selectedRows = event.getParam('selectedRows');
        cmp.set('v.selectedRowsCount', selectedRows.length); 
    },
    
    selectedRecords : function(cmp, event, helper) {
        helper.mergeRecords(cmp, event);   // calling mergeRecords method in helper controller    
    },
    onSave: function (cmp, event, helper) {
        helper.onDraftSave(cmp, event, helper); // calling onDraftSave method in helper controller
    }
})


4. FindDuplicateHelper.js

({
    /* ************************* ABOUT getDuplicateLeads *************************
     * 
     * What's the purpose of developing this 
     *   - Regular Automation Lightning component Duplicate rule doesn't allow to do cross field mapping to check duplicate Lead
     *     Considere that on Lead object you have similar datatype fields: phone and MobilePhone. 
     *     Also another set of similar datatype fields: email and Alternate email.  
     *     For example: There is Lead 1 , Lead 2, Lead 3. if Lead 1 Phone field is matching with Lead 2 MobilPhone field than it should
     *                  be considered as a duplicate lead. Correct ? Same way if value of email field in Lead 1 is matching with value of
     *                  alternate email field in Lead 3 than again it should be considered as duplicate Lead. Correct ?
     *                  Answer is Yes! But Salesforce duplicate rule doesn't allow us to map Phone field to MobilePhone field
     *			and Email field to Alt Email Field. 
     *
     * getDuplicateLeads function will get call on on page load from 'aura:handler init'   
     * 1. Setting up all required column name in lightning:datatable using attribute 'colums'
     * 2. Get current lead recordId and pass it to apex server method to find duplicate leads for this current lead 
     * 3. Getting response back from Apex server (All duplicate leads) and passing it to 'lead attribute'    
     *					   
     */
    
    getDuplicateLeads : function(cmp, event, helper) {
        // step 1
        cmp.set('v.columns', [
            {label: 'Id', fieldName: 'Id', type: 'Id', sortable:true, initialWidth: 168},
            {label: 'CreatedDate', fieldName: 'CreatedDate', type: 'date', typeAttributes: {
                day: 'numeric',
                month: 'short',
                year: 'numeric',
                hour: '2-digit',
                minute: '2-digit',
                second: '2-digit',
            }, sortable:true, initialWidth: 185},
            {label: 'Owner Name', fieldName: 'LeadOwnerName__c', type: 'string',sortable:true, initialWidth: 155},
            {label: 'Name', fieldName: 'leadName', type: 'url', sortable:true, 
             typeAttributes: {label: {fieldName: 'Name'}}, initialWidth: 142},
            {label: 'Lead Status', fieldName: 'Status', type: 'picklist',sortable:true, initialWidth: 155},
            {label: 'Phone', fieldName: 'Phone', type: 'phone',sortable:true, initialWidth: 142, editable: true},
            {label: 'MobilePhone', fieldName: 'MobilePhone', type: 'phone',sortable:true, initialWidth: 142, editable: true},
            {label: 'Email', fieldName: 'Email', type: 'Email',sortable:true, initialWidth: 230, editable: true},
            {label: 'Alt Email', fieldName: 'Alt_Email__c', type: 'Email',sortable:true, initialWidth: 230, editable: true},
            {label: 'ConvertedBusiness', fieldName: 'AccountName', type: 'url',sortable:true, 
             typeAttributes: {label: {fieldName: 'Convertedbusiness__c'}}, initialWidth: 192},            
        ]);
            
            var action = cmp.get("c.getLeadInfo");          // step 2
            var recordId = cmp.get("v.recordId");           // step 2
            action.setParams({"leadRecordID": recordId});   // step 2
            console.log('Hi I am recordId: ' + recordId);
            // Configure response handler
            action.setCallback(this, function(response) {
            var state = response.getState();
            if(state === "SUCCESS") {
            var records = response.getReturnValue();            
            records.forEach(function(record){
            if(record.ConvertedBusiness__r != undefined && record.ConvertedBusiness__r != ''){
            console.log('Hi I am record : ' + JSON.stringify(record.ConvertedBusiness__r));             
            record.leadName = '/'+record.Id;
            }
            
            if(record.ConvertedBusiness__r != undefined && record.ConvertedBusiness__r != ''){
            record.AccountName = '/'+record.ConvertedBusiness__c;
            console.log('AccountName : ' +  record.AccountName);
            }
            });
            cmp.set("v.lead", response.getReturnValue()); // step 3
            console.log('Hi I am all matching leads :' + response.getReturnValue());
            console.log('Hi I am stringify version of all matching leads : ' + JSON.stringify(response.getReturnValue()));
            } 
            else {
            console.log('Problem getting Duplicate Lead, Please check back later! :' + state);
            }
            });
            $A.enqueueAction(action); 
            },
            
        /* ************************* ABOUT mergeRecords *************************
         * mergeRecords will keep one masterRecord that you will select to keep it as a master and will delete all other similar records
         * from Lead object
         * Steps to consider before selecting merge
         * A. Select all checkbox next to record to be considered in merge operation.
         * B. Make All necessary changes to record that you want to keep as Master record using inline edit lightnint:datatable functionality
         * C. Copy that master Lead Id that you want to keep and paste in 'Enter Master Record ID' field.
         * D. Click on merge and you will see success message after quick page reload!   
         */
            mergeRecords : function(cmp, event, helper){
            let allSelectedRows = [];
                let myMap = new Map();
        var mapToSend = {};
        allSelectedRows = cmp.find('selectedRows').getSelectedRows();
        console.log('Hi I am selectedRows : ' + allSelectedRows);
        console.log('Hi I am selectedRows Json String : ' + JSON.stringify(allSelectedRows));
        console.log('Hi I am master Record : ' + cmp.get("v.masterRecId"));
        var masterRecId = cmp.get("v.masterRecId");
        if(allSelectedRows.length > 0){
            if(JSON.stringify(allSelectedRows).indexOf(masterRecId) > -1){
                myMap.set(masterRecId, "True");
                for(var i = 0; i<allSelectedRows.length; i++){
                    if(allSelectedRows[i].Id != masterRecId){
                        myMap.set(allSelectedRows[i].Id, "False");
                    }
                }
                console.log()
                for(var key of myMap.keys()){
                    mapToSend[key] = myMap.get(key);
                }
                var action = cmp.get("c.getMasterRecordEdit");
                console.log('Hi I am myMap to check all key and values : ' + mapToSend);
                action.setParams({"myMap": mapToSend});
                action.setCallback(this, function(response) {
                    var state = response.getState();
                    if(state === "SUCCESS") {
                        if(response.getReturnValue() === true){
                            console.log('Finally I am in true loop');
                            this.finishOperation(cmp, event);
                        }
                        else {
                        }
                    } else {
                        console.log('Problem getting leads, please check back later: ' + state);
                    }
                });
                $A.enqueueAction(action); 
            }
            
            else {
                alert('Please select Master Record in table in order to merge records!');
            }
            
            
            myMap.forEach(function(value, key){
                console.log(key + ' = ' + value)
            })
        }
        else {
            alert('Please select at least two records to merge including Master Record');
        }
    },
    
    /* ************************* ABOUT finishOperation and showToast function ************************* 
     
     * finishOperation function will get called from two different function. This method redirect page to current
     * record page or master record page, depends on from which function it's gets rendered from. 
     
     * 1. If this fucntion gets called from mergeRecords function then decisionMaker attribute will 
     *    stay default boolean to 'false' and after merge page will redirect to Master Record 	 
     * 2. If this function gets called from onDraftSave function then decisionMaker attribute will
     * 	  have boolean value to 'true' and after save page will redirect to current record
     * 3a. If it's inside decisionMaker === true loop, pass current recordId to navigate to current record page after 'save' button click
     *     in lightning:datatable 
     * 3b. If it's inside decisionMaker === false loop, pass Master recordId to navigate to master record page after merge button click 
     * 4. After navigation to record page show message by calling showToast function.
      
     */
    finishOperation : function(cmp, event) {
        
        var decisionMaker = cmp.get("v.funCallDecision");
        var navEvt = $A.get("e.force:navigateToSObject");
        
        console.log('I am decisionMaker : ' + decisionMaker);
        
        if(decisionMaker === true){   // step 3a         
            var recordId = cmp.get("v.recordId"); // step 3a
            navEvt.setParams({
                "recordId": recordId,
                "slideDevName": "related"
            });
            navEvt.fire();
            this.showToast(cmp, event); 
        }
        else if(decisionMaker === false) { //step 3b
            var masterRecId= cmp.get("v.masterRecId"); // step 3b
            navEvt.setParams({
                "recordId": masterRecId,
                "slideDevName": "related"
            });
            navEvt.fire();
            this.showToast(cmp, event);
        }
    },
    
    showToast : function(cmp, event) { // step 4
        var toastEvent = $A.get("e.force:showToast");
        toastEvent.setParams({
            "type": "success",
            "message": "Operation completed Successfully"
        });
        toastEvent.fire();
    },
    
    
    /* ************************* ABOUT onDraftSave function *************************
     * 
     * calling this onDraftSave function to update inline edit changes in server
     * 1. Getting all data using event from 'draftValues' attribute to 'draftValue' javaScript controller variable    
     * 2. After receiving success response from server set attribute funCallDecision to true.
     * 3. Now call finishOperation function which will navigate page to current record (Kind of reloading page) and make changes appear 
     *    in lightning:datatable 
     */    
    onDraftSave : function(cmp, event) {
        var draftValues = event.getParam('draftValues'); // step 1
        console.log('Hi I am draftValues : ' + JSON.stringify(draftValues));
        var action = cmp.get("c.updateLeadRecord");
        action.setParams({"leadDraftRecord" : draftValues});
        action.setCallback(this, function(response) {
            var state = response.getState();
            if(state === "SUCCESS"){
                if(response.getReturnValue() === true) {
                    cmp.set("v.funCallDecision", true); // step 2
                    console.log('Success bc success');
                    this.finishOperation(cmp, event);   // step 3 
                }
            }
        });
        $A.enqueueAction(action);       
    },    
})


5. FindDuplicate.css

.THIS .slds-box{
    background-color: white;
}

.THIS .slds-box-width{
    width: 250px;
    margin-top: 10px;
}

VIDEO

MOST VIEW POST

RECOMMENDED POST

POST COMMENT

Please to continue.