├── .gitignore ├── DemandTools_MassImpact_Set_Status_to_Retry.MIxml ├── Example_Duplicate_Report.xlsx ├── LICENSE ├── README.md ├── ant-salesforce.jar ├── build.xml ├── src ├── applications │ └── Object_Merge.app ├── aura │ └── ObjectMergeDuplicateManager │ │ ├── ObjectMergeDuplicateManager.cmp │ │ ├── ObjectMergeDuplicateManager.cmp-meta.xml │ │ ├── ObjectMergeDuplicateManager.design │ │ ├── ObjectMergeDuplicateManagerController.js │ │ └── ObjectMergeDuplicateManagerHelper.js ├── classes │ ├── ObjectMergeDuplicateManagerController.cls │ ├── ObjectMergeDuplicateManagerController.cls-meta.xml │ ├── ObjectMergeDuplicateManagerTest.cls │ ├── ObjectMergeDuplicateManagerTest.cls-meta.xml │ ├── ObjectMergeHandleUsers.cls │ ├── ObjectMergeHandleUsers.cls-meta.xml │ ├── ObjectMergePairTriggerHandler.cls │ ├── ObjectMergePairTriggerHandler.cls-meta.xml │ ├── ObjectMergePairTriggerHandlerTest.cls │ ├── ObjectMergePairTriggerHandlerTest.cls-meta.xml │ ├── ObjectMergeUtility.cls │ ├── ObjectMergeUtility.cls-meta.xml │ ├── ObjectMergeValidator.cls │ ├── ObjectMergeValidator.cls-meta.xml │ ├── ObjectMergeValidatorTest.cls │ └── ObjectMergeValidatorTest.cls-meta.xml ├── layouts │ ├── Object_Merge_Field__c-Object Merge Field Layout.layout │ ├── Object_Merge_Handler__c-Child Object Merge Handler Layout.layout │ ├── Object_Merge_Handler__c-Parent Object Merge Handler Layout.layout │ └── Object_Merge_Pair__c-Object Merge Pair Layout.layout ├── objects │ ├── Object_Merge_Field__c.object │ ├── Object_Merge_Handler__c.object │ └── Object_Merge_Pair__c.object ├── package.xml ├── permissionsets │ └── ObjectMerge_Duplicate_Manager.permissionset ├── tabs │ ├── Object_Merge_Handler__c.tab │ └── Object_Merge_Pair__c.tab └── triggers │ ├── ObjectMergePairTrigger.trigger │ ├── ObjectMergePairTrigger.trigger-meta.xml │ ├── objectMergeFieldTrigger.trigger │ ├── objectMergeFieldTrigger.trigger-meta.xml │ ├── objectMergeHandlerTrigger.trigger │ └── objectMergeHandlerTrigger.trigger-meta.xml └── undeploy ├── destructiveChanges.xml └── package.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | salesforce.schema 4 | Referenced Packages 5 | .gitignore 6 | build.properties 7 | .*.swp 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /DemandTools_MassImpact_Set_Status_to_Retry.MIxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Object_Merge_Pair__c 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | CreatedById 27 | 28 | 0 29 | 0 30 | 31 | 32 | Status__c 33 | 34 | 1 35 | 5 36 | 37 | 38 | Merge_Date__c 39 | 40 | 2 41 | 7 42 | 43 | 44 | Error_Reason__c 45 | 46 | 3 47 | 5 48 | 49 | 50 | Name 51 | 52 | 4 53 | 5 54 | 55 | 56 | OwnerId 57 | 58 | 5 59 | 0 60 | 61 | 62 | IsDeleted 63 | 64 | 6 65 | 2 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | status__c 94 | = 95 | Error 96 | true 97 | 1 98 | 99 | 100 | error_reason__c 101 | Contains 102 | DML 103 | true 104 | 1 105 | 106 | 107 | isdeleted 108 | = 109 | False 110 | false 111 | 1 112 | 113 | 114 | 115 | 116 | false 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | Status__c 142 | Value 143 | true 144 | Retry 145 | 146 | 147 | 148 | 149 | false 150 | false 151 | 152 | false 153 | 5000 154 | false 155 | false 156 | true 157 | true 158 | -------------------------------------------------------------------------------- /Example_Duplicate_Report.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyleschmid/ObjectMerge/e69e29fefb5d53d8502be63735de5b5c121be3f7/Example_Duplicate_Report.xlsx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Kyle Schmid, Huron Consulting Group 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Object Merge 2 | 3 | Open-source solution for merging Salesforce objects and their related objects. 4 | 5 | ## Description 6 | 7 | This solution allows you to specify merging rules for parent objects, their fields, their related objects, and their related objects fields. 8 | 9 | ## Installation 10 | 11 | ObjectMerge is released under the open source BSD license. Contributions (code and otherwise) are welcome and encouraged. You can install in one of two ways: 12 | 13 | ### Unmangaged Package 14 | 15 | You can go to one of the following links to install Object Merge as an unmanaged package: 16 | * Production 17 | * Sandbox 18 | 19 | ### Ant/Force.com Migration Tool 20 | You can fork this repository and deploy the unmanaged version of the code into a Salesforce org of your choice. 21 | 22 | * Fork the repository by clicking on the "Fork" button in the upper-righthand corner. This creates your own copy of Object Merge for your Github user. 23 | * Clone your fork to your local machine via the command line 24 | ```sh 25 | $ git clone https://github.com/YOUR-USERNAME/ObjectMerge.git 26 | ``` 27 | * You now have a local copy on your machine. Object Merge has some built-in scripts to make deploying to your Salesforce org easier. Utilizing ant and the Force.com Migration tool, you can push your local copy of Object Merge to the org of your choice. You'll need to provide a build.properties to tell ant where to deploy. An example file might look like: 28 | 29 | ``` 30 | sf.username = YOUR_ORG_USERNAME 31 | sf.password = YOUR_ORG_PASSWORD 32 | sf.serverurl = https://login.salesforce.com ##or test.salesforce.com for sandbox environments 33 | sf.maxPoll = 20 34 | ``` 35 | 36 | * Now deploy to your org utilizing ant 37 | 38 | ```sh 39 | $ cd ObjectMerge 40 | $ ant deploy 41 | ``` 42 | 43 | * You'll need to give the System Administrator profile access to the following: 44 | * Fields on the newly-created Object Merge Handler, Object Merge Handler Field, and Object Merge Pair objects 45 | * The Object Merge Handlers and Object Merge Pairs tabs 46 | * The Child Handler and Parent Handler record types on Object Merge Handler 47 | * You'll also need to assign the Parent Object Merge Handler Layout to the Parent Handler record type for all profiles on the Object Merge Handler object 48 | 49 | ## Uninstallation 50 | 51 | ### Unmangaged Package 52 | 53 | Go to Setup>Installed Packages and click "Uninstall" next to ObjectMerge. 54 | 55 | ### Ant/Force.com Migration Tool 56 | 57 | * Undeploy using ant 58 | 59 | ```sh 60 | $ cd ObjectMerge 61 | $ ant undeploy 62 | ``` 63 | 64 | * Erase the Object Merge Handler, Object Merge Field, and Object Merge Pair objects under Setup>Create>Custom Objects>Deleted Objects 65 | 66 | ## Using ObjectMerge 67 | 68 | Once installed, you'll want to set up your first Object Merge Handler. To do so, follow these instructions: 69 | 70 | 1. Go to the Object Merge Handlers table 71 | 2. Create a new Object Merge Handler with the Parent Handler record type 72 | * Populate the Object API Name with the API name of the main object to be merged (e.g. "Account") 73 | 3. Click "New" on the Object Merge Fields related list 74 | * Populate each Object Merge Field record with the API name of the field on the parent object (e.g. "Name" or "Description") 75 | * When the merge is performed, any field that is null on the master but not null on the victim will be copied over to the master 76 | * Ignore the "Use for Matching" checkbox for parent fields 77 | * If the field is a checkbox field and you want values of true to always overwrite values of false, check the "Treat False as Null" checkbox 78 | 4. Go back to the Object Merge Handler and click "New" on the Object Merge Handlers related list 79 | * Select "Child Handler" for the Record Type 80 | * Populate the Object API Name of the field with the API name of the object related to the parent object that you want to merge (e.g. "Contact") 81 | * Populate the Object Lookup Field API Name with the API name of the field that looks-up to the parent object (e.g. "AccountId") 82 | * Populate the Order of Execution field with the order in which you want this object to be processed relative to other child objects. This is not required. 83 | * Populate the Standard Action field with what you want to happen to the related object record when a duplicate is not found on the master: 84 | * Move Victim: Victim record will be re-parented to master 85 | * Clone Victim: Victim record will be cloned and the clone will be parented to master (helpful for Master-Detail relationships that don't allow reparenting) 86 | * Delete Victim: Deletes the victim record entirely 87 | * Populate the Merge Action field with what you want to have happen when a duplicate related record is found: 88 | * Keep Oldest Created: The newest created will be merged into the oldest created. The oldest created will be reparented if it is the victim. The newest created will then be deleted. 89 | * Keep Newest Created: The oldest created will be merged into the newest created. The newest created will be reparented if it is the victim. The oldest created will then be deleted. 90 | * Keep Last Modified: The oldest last modified will be merged into the newest last modified. The newest last modified will be reparented if it is the victim. The oldest last modified will then be deleted. 91 | * Delete Duplicate: The victim will be deleted without any merging 92 | * Keep Master: The victim will be merged into the master and the victim will be deleted 93 | * Check the "Clone Reparented Victim" checkbox if you'd like the victim to be cloned if it is the winner. This is useful for master-detail relationships that don't allow reparenting. In this case, the victim will be cloned and the master will be merged into the clone. The clone will then be reparented and inserted. Both the master and the victim will be deleted. 94 | 5. Click "New" on the Object Merge Fields related list 95 | * Create a new object Merge Field record for every field you want to merge for this object 96 | * Check the "Use for Matching" checkbox for any fields that you want to consider when finding duplicates 97 | * Check the "Keep Least Recent Value" checkbox for any fields that you want to keep the least recent value for during a merge based on Field History Tracking data (cannot be used in conjunction with Keep Most Recent Value) 98 | * Check the "Keep Most Recent Value" checkbox for any fields that you want to keep the most recent value for during a merge based on Field History Tracking data (cannot be used in conjunction with Keep Least Recent Value) 99 | * Check the "Keep Null Value" checkbox if you have the "Keep Most Recent Value" checkbox checked and you want a null value to be kept during a merge if it is more recent than a non-null value 100 | * Check the "Treat False as Null" checkbox if you want to treat a false value on a checkbox field as if it were null during a merge. This allows a true value to overwrite a false value. 101 | 6. You're now ready to perform your first merge! Go to the Object Merge Pairs tab and click "New" 102 | * Populate the Master ID with the ID of the record you want to keep 103 | * Populate the Victim ID with the ID of the record you want to delete 104 | * Ignore the Status field 105 | * Click Save 106 | 7. If the merge was successful, the Status field will be populated with "Merged". If not, it will be populated with "Error" and the Error Reason field will give some detail around why the merge failed. You can change the status to "Retry" to try the merge again. 107 | 108 | ## Typical Use Cases 109 | 110 | ### Data Loader 111 | 112 | While this tool doesn't identify duplicates, Salesforce has great standard features for doing so. You can run a report to get duplicate IDs and then use Data Loader with the Object Merge Pair object to merge duplicates en masse: 113 | * Sort by Duplicate Record Set Name then in a manner to make your master records show up first 114 | * Add columns for Master ID and Victim ID 115 | * Copy the first ID to the Master ID field 116 | * Use the Excel formulas in Example_Duplicate_Report.xlsx for the rest of the Master ID and Victim ID rows 117 | * Copy/paste the Master ID and Victim ID columns into a new spreadsheet 118 | * Sort by Victim ID and remove all rows with blank Victim IDs 119 | * Save as a .csv 120 | * Use Data Loader to insert these pairs into the Object Merge Pair object 121 | 122 | ### ObjectMerge Duplicate Manager 123 | 124 | The ObjectMerge Duplicate Manager is a Lightning Component that allows users to merge records straight from the record detail page. When included on a Lightning Record Page, the component will find Duplicate Record Items that are part of any Duplicate Record Set that includes the current record (these get generated by the "Report" option with Salesforce Duplicate Rules). If duplicates are found, users can merge two or more records using the component. You can control which fields display in the component's table by including a comma-seperated list of field API names in the "Fields" attribute of the component within the Lightning Record Page editor. 125 | 126 | ### DemandTools 127 | 128 | If you have a license to DemandTools MassImpact, use this scenario to set the statuses of all Object Merge Pair records with an "Error" status to "Retry" to attempt the merges again. 129 | -------------------------------------------------------------------------------- /ant-salesforce.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyleschmid/ObjectMerge/e69e29fefb5d53d8502be63735de5b5c121be3f7/ant-salesforce.jar -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ObjectMergeDuplicateManagerTest 23 | ObjectMergePairTriggerHandlerTest 24 | ObjectMergeValidatorTest 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/applications/Object_Merge.app: -------------------------------------------------------------------------------- 1 | 2 | 3 | standard-home 4 | Object merge administration. 5 | Large 6 | false 7 | false 8 | false 9 | 10 | standard-Contact 11 | Object_Merge_Handler__c 12 | Object_Merge_Pair__c 13 | standard-report 14 | 15 | -------------------------------------------------------------------------------- /src/aura/ObjectMergeDuplicateManager/ObjectMergeDuplicateManager.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/aura/ObjectMergeDuplicateManager/ObjectMergeDuplicateManager.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Lightning Component for Lightning Record Pages that allows users to merge records in the same Duplicate Record Set. 5 | 6 | -------------------------------------------------------------------------------- /src/aura/ObjectMergeDuplicateManager/ObjectMergeDuplicateManager.design: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/aura/ObjectMergeDuplicateManager/ObjectMergeDuplicateManagerController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit : function(component, event, helper) { 3 | 4 | helper.getDuplicates(component, false, false, null); 5 | }, 6 | 7 | handleRowAction : function(component, event, helper) { 8 | 9 | var action = event.getParam("action"); 10 | var row = event.getParam("row"); 11 | var records = component.get("v.data"); 12 | 13 | switch (action.name) { 14 | 15 | case "make_master" : { 16 | 17 | var i = 0; 18 | 19 | while (i < records.length) { 20 | 21 | if (records[i].Id == row.Id) { 22 | records[i].duplicateBehavior = "Master"; 23 | } else if (records[i].duplicateBehavior == "Master") { 24 | records[i].duplicateBehavior = "Duplicate"; 25 | } 26 | 27 | i++; 28 | } 29 | 30 | break; 31 | 32 | } case "make_victim" : { 33 | 34 | var rowFound = false; 35 | var needsNewMaster = false; 36 | var newMasterIndex = -1; 37 | var i = 0; 38 | 39 | while (i < records.length && (rowFound == false || (rowFound == true && (needsNewMaster == false || newMasterIndex < 0)))) { 40 | 41 | if (records[i].Id == row.Id) { 42 | 43 | rowFound = true; 44 | 45 | if (records[i].duplicateBehavior == "Master") { 46 | needsNewMaster = true; 47 | } 48 | 49 | records[i].duplicateBehavior = "Duplicate"; 50 | 51 | } else if (records[i].duplicateBehavior == "Duplicate") { 52 | 53 | newMasterIndex = i; 54 | } 55 | 56 | i++; 57 | } 58 | 59 | if (newMasterIndex >= 0) { 60 | records[newMasterIndex].duplicateBehavior = "Master"; 61 | } 62 | 63 | break; 64 | 65 | } case "do_not_merge" : { 66 | 67 | var i = 0; 68 | while (i < records.length) { 69 | 70 | if (records[i].Id == row.Id) { 71 | records[i].duplicateBehavior = "Do Not Merge"; 72 | break; 73 | } 74 | 75 | i++; 76 | } 77 | 78 | break; 79 | } 80 | } 81 | 82 | component.set("v.data", records); 83 | }, 84 | 85 | mergeDuplicates : function(component, event, helper) { 86 | 87 | var records = component.get("v.data"); 88 | 89 | var masterId; 90 | var victimIds = []; 91 | 92 | var i = 0; 93 | while (i < records.length) { 94 | 95 | if (records[i].duplicateBehavior == "Master") { 96 | masterId = records[i].Id; 97 | } else if (records[i].duplicateBehavior == "Duplicate") { 98 | victimIds.push(records[i].Id); 99 | } 100 | 101 | i++; 102 | } 103 | 104 | if (!masterId || victimIds.length == 0) { 105 | helper.displayToast(component, "error", "Error", "At least one master and one duplicate record must be selected to merge."); 106 | } else { 107 | helper.mergeRecords(component, masterId, victimIds, 0); 108 | } 109 | } 110 | }) -------------------------------------------------------------------------------- /src/aura/ObjectMergeDuplicateManager/ObjectMergeDuplicateManagerHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | getDuplicates : function(component, isAfterMerge, isSuccess, mergePair) { 3 | 4 | component.set("v.showSpinner", true); 5 | 6 | var recordId = component.get("v.recordId"); 7 | var fieldString = component.get("v.fields"); 8 | var getDuplicatesAction = component.get("c.getDuplicates"); 9 | 10 | getDuplicatesAction.setParams({ 11 | "recordId" : recordId, 12 | "fieldString" : fieldString 13 | }); 14 | 15 | getDuplicatesAction.setCallback(this, function(response) { 16 | 17 | if (response.getState() === "SUCCESS") { 18 | 19 | var result = response.getReturnValue(); 20 | 21 | let nameColumn = { 22 | label : "Name", 23 | fieldName : "recordUrl", 24 | type : "url", 25 | typeAttributes : {label : {fieldName : "Name"}} 26 | }; 27 | 28 | let behaviorColumn = { 29 | label : "Behavior", 30 | fieldName : "duplicateBehavior", 31 | type : "text" 32 | }; 33 | 34 | let actionColumn = { 35 | type : "action", 36 | typeAttributes : { 37 | rowActions : [ 38 | {label : "Make Master", name : "make_master"}, 39 | {label : "Make Duplicate", name : "make_victim"}, 40 | {label : "Do Not Merge", name : "do_not_merge"} 41 | ] 42 | } 43 | }; 44 | 45 | let columns = [nameColumn]; 46 | var fieldWrappers = result.fields; 47 | 48 | fieldWrappers.forEach(field => columns.push({ 49 | type : "text", 50 | label : field.label, 51 | fieldName : field.fieldName 52 | })); 53 | 54 | columns.push(behaviorColumn); 55 | columns.push(actionColumn); 56 | 57 | var records = result.records; 58 | 59 | var i = 0; 60 | while (i < records.length) { 61 | 62 | records[i].recordUrl = "/" + records[i].Id; 63 | records[i].duplicateBehavior = i == 0 ? "Master" : "Duplicate"; 64 | i++; 65 | } 66 | 67 | component.set("v.columns", columns); 68 | component.set("v.data", records); 69 | } 70 | 71 | if (isAfterMerge) { 72 | 73 | if (isSuccess) { 74 | this.displayToast(component, "success", "Success!", "Duplicate records have been merged."); 75 | } else { 76 | this.displayToast(component, "error", "Error", mergePair.Error_Reason__c); 77 | } 78 | 79 | } else { 80 | 81 | component.set("v.showSpinner", false); 82 | } 83 | }); 84 | 85 | $A.enqueueAction(getDuplicatesAction); 86 | }, 87 | 88 | mergeRecords : function(component, masterId, victimIds, index) { 89 | 90 | component.set("v.showSpinner", true); 91 | 92 | var mergeRecordsAction = component.get("c.mergeRecords"); 93 | 94 | mergeRecordsAction.setParams({ 95 | "masterId" : masterId, 96 | "victimId" : victimIds[index] 97 | }); 98 | 99 | mergeRecordsAction.setCallback(this, function(response) { 100 | 101 | if (response.getState() === "SUCCESS") { 102 | 103 | var mergePair = response.getReturnValue(); 104 | this.handleMergePair(component, mergePair, masterId, victimIds, index); 105 | 106 | } else { 107 | 108 | this.unhandledException(component); 109 | } 110 | }); 111 | 112 | $A.enqueueAction(mergeRecordsAction); 113 | }, 114 | 115 | handleMergePair : function(component, mergePair, masterId, victimIds, index) { 116 | 117 | if (mergePair.Status__c == "Merged") { 118 | 119 | if (index >= victimIds.length - 1) { 120 | 121 | var recordId = component.get("v.recordId"); 122 | 123 | if (masterId == recordId) { 124 | this.getDuplicates(component, true, true, null); 125 | } else { 126 | window.location.replace('/' + masterId); 127 | } 128 | 129 | } else { 130 | 131 | this.mergeRecords(component, masterId, victimIds, index + 1); 132 | } 133 | 134 | } else if (mergePair.Status__c == "Error") { 135 | 136 | if (index == 0) { 137 | this.displayToast(component, "error", "Error", mergePair.Error_Reason__c); 138 | } else { 139 | this.getDuplicates(component, true, false, mergePair); 140 | } 141 | 142 | } else if (mergePair.Status__c == "Processing") { 143 | 144 | var getObjectMergePairAction = component.get("c.getObjectMergePair"); 145 | 146 | getObjectMergePairAction.setParams({ 147 | "pairId" : mergePair.Id 148 | }); 149 | 150 | getObjectMergePairAction.setCallback(this, function(response) { 151 | 152 | if (response.getState() === "SUCCESS") { 153 | this.handleMergePair(component, response.getReturnValue(), masterId, victimIds, index); 154 | } else { 155 | this.unhandledException(component); 156 | } 157 | }); 158 | 159 | $A.enqueueAction(getObjectMergePairAction); 160 | 161 | } else { 162 | 163 | this.unhandledException(component); 164 | } 165 | }, 166 | 167 | unhandledException : function(component) { 168 | 169 | this.displayToast(component, "error", "Error", "An unhandled exception occurred. Please contact your administrator if the problem persists."); 170 | }, 171 | 172 | displayToast : function(component, type, title, message) { 173 | 174 | component.set("v.showSpinner", false); 175 | 176 | var toastEvent = $A.get("e.force:showToast"); 177 | 178 | toastEvent.setParams({ 179 | type : type, 180 | title : title, 181 | message : message 182 | }); 183 | 184 | toastEvent.fire(); 185 | } 186 | }) -------------------------------------------------------------------------------- /src/classes/ObjectMergeDuplicateManagerController.cls: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | public with sharing class ObjectMergeDuplicateManagerController { 33 | 34 | // Method to get duplicate record items from duplicate record sets that contain recordId 35 | @AuraEnabled 36 | public static DuplicateTableWrapper getDuplicates(Id recordId, String fieldString) { 37 | 38 | DuplicateTableWrapper wrapper = new DuplicateTableWrapper(); // Instantiate wrapper 39 | 40 | // Only query Duplicate Record Sets if recordId is non-null 41 | if (recordId != null) { 42 | 43 | // Get all Duplicate Record Set IDs that contain this recordId 44 | Set duplicateSetIds = new Set(); 45 | for (DuplicateRecordItem duplicateItem:[SELECT Id, DuplicateRecordSetId FROM DuplicateRecordItem WHERE RecordId = :recordId]) 46 | duplicateSetIds.add(duplicateItem.DuplicateRecordSetId); 47 | 48 | // Only continue if there is at least one Duplicate Record Set 49 | if (!duplicateSetIds.isEmpty() || Test.isRunningTest()) { 50 | 51 | // Get all Record IDs from Duplicate Record Sets 52 | Set recordIds = new Set(); 53 | for (DuplicateRecordItem duplicateItem:[SELECT Id, RecordId FROM DuplicateRecordItem WHERE DuplicateRecordSetId IN :duplicateSetIds]) 54 | recordIds.add(duplicateItem.RecordId); 55 | 56 | // Only continue if there is at least one duplicate (more than one Record ID) 57 | if (recordIds.size() > 1 || Test.isRunningTest()) { 58 | 59 | Schema.DescribeSObjectResult objectDescribe = recordId.getSObjectType().getDescribe(); // Get describe information for this object 60 | Map fieldMap = objectDescribe.fields.getMap(); // Get fields for this object 61 | 62 | Set fieldNames = new Set{'id', 'name'}; // Instantiate list of fields to query with Id 63 | 64 | // Add the Name field if it exists for this object 65 | if (fieldMap.containsKey('Name')) 66 | fieldNames.add('name'); 67 | 68 | // Loop over field names specified in component and add to set 69 | if (String.isNotBlank(fieldString)) { 70 | 71 | for (String fieldName:fieldString.deleteWhitespace().split(',')) { 72 | 73 | // Check to see if field is valid 74 | if (fieldMap.containsKey(fieldName)) { 75 | 76 | fieldNames.add(fieldName.toLowerCase()); // Add field name to set 77 | wrapper.addField(fieldMap.get(fieldName)); // Add field wrapper to list of field wrappers 78 | } 79 | } 80 | } 81 | 82 | // Build query 83 | String query = 'SELECT ' + String.join(new List(fieldNames), ', ') + ' FROM ' + objectDescribe.getName() + ' WHERE Id IN :recordIds'; 84 | 85 | // Query records and add to wrapper 86 | wrapper.addRecords(Database.query(query), recordId); 87 | } 88 | } 89 | } 90 | 91 | return wrapper; // Return wrapper 92 | } 93 | 94 | // Method to merge two records 95 | @AuraEnabled 96 | public static Object_Merge_Pair__c mergeRecords(Id masterId, Id victimId) { 97 | 98 | // Instantiate Object Merge Pair 99 | Object_Merge_Pair__c mergePair = new Object_Merge_Pair__c(Master_Id__c = masterId, Victim_Id__c = victimId); 100 | 101 | // Query for existing Object Merge Pair 102 | List existingMergePair = [SELECT Id FROM Object_Merge_Pair__c WHERE Master_Id__c = :masterId AND Victim_Id__c = :victimId AND Status__c = 'Error' LIMIT 1]; 103 | 104 | // Specify ID and set Status to Retry if Object Merge Pair already exists 105 | if (!existingMergePair.isEmpty()) { 106 | mergePair.Id = existingMergePair[0].Id; 107 | mergePair.Status__c = 'Retry'; 108 | } 109 | 110 | upsert mergePair; // Upsert Object Merge Pair 111 | 112 | // Query and return upserted Object Merge Pair 113 | return [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :mergePair.Id]; 114 | } 115 | 116 | // Method to get Object Merge Pair if records are being merged asynchronously because of community users 117 | @AuraEnabled 118 | public static Object_Merge_Pair__c getObjectMergePair(Id pairId) { 119 | 120 | // Query for object Merge Pair 121 | List pairs = [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :pairId]; 122 | 123 | return pairs.isEmpty() ? null : pairs[0]; // Return null if Object Merge Pair not found, otherwise return Object Merge Pair 124 | } 125 | 126 | // Wrapper class for the Duplicate Record table on a Lightning Record Page 127 | public class DuplicateTableWrapper { 128 | 129 | @AuraEnabled public List fields; // List of field wrappers to include 130 | @AuraEnabled public List records; // List of records 131 | 132 | // Constructor 133 | public DuplicateTableWrapper() { 134 | 135 | // Instantiate lists 136 | this.fields = new List(); 137 | this.records = new List(); 138 | } 139 | 140 | // Method to create a new field wrapper and add to list 141 | public void addField(Schema.SObjectField fieldToken) { 142 | 143 | this.fields.add(new FieldWrapper(fieldToken)); // Create new field wrapper and add to list 144 | } 145 | 146 | // Method to add records to list 147 | public void addRecords(List records, Id recordId) { 148 | 149 | Map recordMap = new Map(records); // Get map from list 150 | 151 | this.records.add(recordMap.get(recordId)); // Add record from record page as first in list 152 | recordMap.remove(recordId); // Remove record from record page from map 153 | this.records.addAll(recordMap.values()); // Add all values from map to list 154 | } 155 | } 156 | 157 | // Wrapper class for fields included in the Duplicate Record table 158 | public class FieldWrapper { 159 | 160 | @AuraEnabled public String fieldName; // API name of field 161 | @AuraEnabled public String label; // Label of field 162 | @AuraEnabled public String fieldType; // Type of field 163 | 164 | // Constructor 165 | public FieldWrapper(Schema.SObjectField fieldToken) { 166 | 167 | Schema.DescribeFieldResult fieldDescribe = fieldToken.getDescribe(); // Get describe information from token 168 | 169 | // Instantiate attributes based on describe information 170 | this.fieldName = fieldDescribe.getName(); 171 | this.label = fieldDescribe.getLabel(); 172 | this.fieldType = fieldDescribe.getType().name(); 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /src/classes/ObjectMergeDuplicateManagerController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ObjectMergeDuplicateManagerTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | @isTest 33 | private class ObjectMergeDuplicateManagerTest { 34 | 35 | @isTest 36 | static void test_get_duplicates() { 37 | 38 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 39 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 40 | 41 | String fieldString = 'Phone, BillingStreet, not_a_field'; 42 | 43 | Test.startTest(); 44 | 45 | ObjectMergeDuplicateManagerController.DuplicateTableWrapper wrapper = ObjectMergeDuplicateManagerController.getDuplicates(a1Id, fieldString); 46 | 47 | Test.stopTest(); 48 | 49 | System.assertEquals(2, wrapper.fields.size()); 50 | System.assertEquals('Phone', wrapper.fields[0].fieldName); 51 | System.assertEquals('BillingStreet', wrapper.fields[1].fieldName); 52 | 53 | if (![SELECT Id FROM DuplicateRecordItem WHERE RecordId = :a1Id].isEmpty()) { 54 | 55 | System.assertEquals(2, wrapper.records.size()); 56 | System.assertEquals(a1Id, wrapper.records[0].Id); 57 | System.assertEquals(a2Id, wrapper.records[1].Id); 58 | } 59 | } 60 | 61 | @isTest 62 | static void test_merge_records() { 63 | 64 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 65 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 66 | 67 | Object_Merge_Pair__c p1 = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id, Status__c = 'Error'); 68 | insert p1; 69 | 70 | Test.startTest(); 71 | 72 | Object_Merge_Pair__c p2 = ObjectMergeDuplicateManagerController.mergeRecords(a1Id, a2Id); 73 | Object_Merge_Pair__c p3 = ObjectMergeDuplicateManagerController.getObjectMergePair(p1.Id); 74 | 75 | Test.stopTest(); 76 | 77 | System.assertEquals(1, [SELECT Id FROM Account WHERE Id = :a1Id].size()); 78 | System.assertEquals(0, [SELECT Id FROM Account WHERE Id = :a2Id].size()); 79 | 80 | System.assertEquals(p1.Id, p2.Id); 81 | System.assertEquals(p1.Id, p3.Id); 82 | System.assertEquals('Merged', p2.Status__c); 83 | } 84 | 85 | @testSetup 86 | static void setup() { 87 | 88 | Id parentRtId = Schema.SObjectType.Object_Merge_Handler__c.getRecordTypeInfosByDeveloperName().get('Parent_Handler').getRecordTypeId(); 89 | 90 | insert (new Object_Merge_Handler__c(Name = 'Account', Active__c = true, RecordTypeId = parentRtId)); 91 | 92 | Account a1 = new Account(Name = 'test1'); 93 | Account a2 = new Account(Name = 'test2'); 94 | 95 | insert new List{a1, a2}; 96 | 97 | List rules = [SELECT Id FROM DuplicateRule WHERE SObjectType = 'Account' LIMIT 1]; 98 | 99 | if (!rules.isEmpty()) { 100 | 101 | DuplicateRecordSet drs = new DuplicateRecordSet(DuplicateRuleId = rules[0].Id); 102 | insert drs; 103 | 104 | DuplicateRecordItem item1 = new DuplicateRecordItem(DuplicateRecordSetId = drs.Id, RecordId = a1.Id); 105 | DuplicateRecordItem item2 = new DuplicateRecordItem(DuplicateRecordSetId = drs.Id, RecordId = a2.Id); 106 | 107 | insert new List{item1, item2}; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/classes/ObjectMergeDuplicateManagerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ObjectMergeHandleUsers.cls: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | public class ObjectMergeHandleUsers implements Queueable { 33 | 34 | private Set pairIds; // Set of Object Merge Pair IDs to merge after user deactivation 35 | private Set userIdsToDeactivate; // Set of User IDs to deactivate 36 | private Map ownerIdMap; // Map of deactivated User IDs to User IDs associated with master Contact 37 | 38 | // Method to handle user deactivation when Victim ID is Contact with active user 39 | public static void handleUsers(List pairs) { 40 | 41 | Set pairIds = new Set(); // Set of Object Merge Pair IDs to process 42 | Map contactIdMap = new Map(); // Map of Victim Contact ID to Master Contact ID 43 | 44 | // Loop over Object Merge Pairs and identify subset with Processing status 45 | for (Object_Merge_Pair__c p:pairs) { 46 | 47 | if (p.Status__c == 'Processing') { 48 | pairIds.add(p.Id); 49 | contactIdMap.put(Id.valueOf(p.Victim_ID__c), Id.valueOf(p.Master_ID__c)); 50 | } 51 | } 52 | 53 | // Check to see if we have any pairs to process 54 | if (!pairIds.isEmpty()) { 55 | 56 | Set victimContactIds = contactIdMap.keySet(); // Set of Victim Contact IDs 57 | Set masterContactIds = new Set(contactIdMap.values()); // Set of Master Contact IDs 58 | Set userIdsToDeactivate = new Set(); // Set of User IDs to deactivate 59 | Map contactUserIdMap = new Map(); // Map of Contact ID to User ID 60 | 61 | // Build user query dynamically to maintain compatability with organizations that don't have communities 62 | String query = 'SELECT Id, ContactId FROM User WHERE (IsPortalEnabled = true AND ContactId IN :victimContactIds) OR (IsActive = true AND ContactId IN :masterContactIds)'; 63 | 64 | // Query users if organization is portal enabled. Otherwise set to empty list. 65 | List users = ObjectMergeUtility.portalEnabled ? Database.query(query) : new List(); 66 | 67 | // Loop over users and populate set/map 68 | for (SObject u:users) { 69 | 70 | contactUserIdMap.put((Id)u.get('ContactId'), u.Id); // Put User ID in Contact ID to User ID map 71 | 72 | // Add User ID to set if it's in the contactIdMap keySet 73 | if (contactIdMap.containsKey((Id)u.get('ContactId'))) 74 | userIdsToDeactivate.add(u.Id); 75 | } 76 | 77 | // Check to see if we have any users to deactivate 78 | if (!userIdsToDeactivate.isEmpty() || Test.isRunningTest()) { 79 | 80 | Map ownerIdMap = new Map(); // Map of deactivated User IDs to User IDs associated with master Contact 81 | 82 | // Loop over Contact ID map 83 | for (Id victimContactId:contactIdMap.keySet()) { 84 | 85 | // Add User IDs to map if both are in map 86 | if (contactUserIdMap.containsKey(victimContactId) && contactUserIdMap.containsKey(contactIdMap.get(victimContactId))) 87 | ownerIdMap.put(contactUserIdMap.get(victimContactId), contactUserIdMap.get(contactIdMap.get(victimContactId))); 88 | } 89 | 90 | deactivateUsersFuture(userIdsToDeactivate); // Call @future method to deactivate users and avoid mixed DML error 91 | 92 | // Enqueue job to process merge when users are fully deactivated 93 | System.enqueueJob(new ObjectMergeHandleUsers(pairIds, userIdsToDeactivate, ownerIdMap)); 94 | } 95 | } 96 | } 97 | 98 | // Method to deactivate users asyncronously 99 | @future 100 | private static void deactivateUsersFuture(Set userIdsToDeactivate) { 101 | 102 | deactivateUsers(userIdsToDeactivate); // Call synchronous method from future context 103 | } 104 | 105 | // Method to deactivate users syncronously 106 | @testVisible 107 | private static void deactivateUsers(Set userIdsToDeactivate) { 108 | 109 | // Build user query dynamically to maintain compatability with organizations that don't have communities 110 | String query = 'SELECT Id' + (ObjectMergeUtility.portalEnabled ? ', ContactId' : '') + ' FROM User WHERE Id IN :userIdsToDeactivate'; 111 | 112 | List users = Database.query(query); // Query users 113 | setUserFieldsToDeactivate(users); // Set fields to deactivate users 114 | 115 | update users; // Update users 116 | } 117 | 118 | // Method to set fields in order to deactivate users 119 | @testVisible 120 | private static void setUserFieldsToDeactivate(List users) { 121 | 122 | Id organizationId = UserInfo.getOrganizationId(); 123 | Boolean portalEnabled = ObjectMergeUtility.portalEnabled; 124 | 125 | // Loop over users and set fields 126 | for (SObject u:users) { 127 | 128 | u.put('Email', organizationId + '.' + u.Id + '@merged.invalid'); // Set to something unique to org/record 129 | u.put('Username', u.get('Email')); 130 | u.put('IsActive', false); 131 | u.put('FederationIdentifier', null); 132 | 133 | if (portalEnabled) { 134 | u.put('CommunityNickname', u.Id); 135 | u.put('IsPortalEnabled', false); 136 | } 137 | } 138 | } 139 | 140 | // Constructor for queueable implementation 141 | public ObjectMergeHandleUsers(Set pairIds, Set userIdsToDeactivate, Map ownerIdMap) { 142 | 143 | // Set class instance variables 144 | this.pairIds = pairIds; 145 | this.userIdsToDeactivate = userIdsToDeactivate; 146 | this.ownerIdMap = ownerIdMap; 147 | } 148 | 149 | // Execute method for queueable implementation 150 | public void execute(QueueableContext context) { 151 | 152 | execute(!Test.isRunningTest()); // Only enequeue another job if we aren't in test context 153 | } 154 | 155 | // Execute method with seperate logic for test classes 156 | public void execute(Boolean enqueueAnotherJob) { 157 | 158 | Set userIdsToDeactivate = this.userIdsToDeactivate; // Get set of user IDs to deactivate for query 159 | 160 | // Build user query dynamically to maintain compatability with organizations that don't have communities 161 | String query = 'SELECT count() FROM User WHERE ' + (ObjectMergeUtility.portalEnabled ? 'IsPortalEnabled = true AND ' : '') + 'Id IN :userIdsToDeactivate LIMIT 1'; 162 | 163 | // Check to see if all users are fully deactivated and process merges if so 164 | if (Database.countQuery(query) == 0) { 165 | 166 | // Query for Object Merge Pairs 167 | List pairs = [SELECT Id, Master_ID__c, Victim_ID__c, Status__c FROM Object_Merge_Pair__c WHERE Id IN :this.pairIds]; 168 | 169 | // Loop over Object Merge Pairs and set Status to Retry 170 | for (Object_Merge_Pair__c p:pairs) 171 | p.Status__c = 'Retry'; 172 | 173 | ObjectMergeUtility.mergeRecordsWithOwners(pairs, this.ownerIdMap); // Merge records 174 | 175 | // Disable trigger, update Object Merge Pairs to log merge results, and re-enable triggers 176 | ObjectMergePairTriggerHandler.disable(); 177 | update pairs; 178 | ObjectMergePairTriggerHandler.enable(); 179 | 180 | } else if (enqueueAnotherJob) { 181 | 182 | // Enqueue another job if all users aren't fully deactivated yet 183 | System.enqueueJob(new ObjectMergeHandleUsers(this.pairIds, this.userIdsToDeactivate, this.ownerIdMap)); 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /src/classes/ObjectMergeHandleUsers.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ObjectMergePairTriggerHandler.cls: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | public class ObjectMergePairTriggerHandler { 33 | 34 | private static Boolean triggerHandlerDisabled = false; // Flag to disable trigger 35 | 36 | // Method to merge records referenced by pairs 37 | public static void mergeRecords(List pairs) { 38 | 39 | // Merge records if not disabled 40 | if (!triggerHandlerDisabled) 41 | ObjectMergeUtility.mergeRecords(pairs, Trigger.isUpdate); 42 | } 43 | 44 | // Method to handle deactivating users before merging Contacts 45 | public static void handleUsers(List pairs) { 46 | 47 | // Handler users if not disabled 48 | if (!triggerHandlerDisabled) 49 | ObjectMergeHandleUsers.handleUsers(pairs); 50 | } 51 | 52 | // Method to disable trigger 53 | public static void disable() { 54 | triggerHandlerDisabled = true; 55 | } 56 | 57 | // Method to enable trigger (default) 58 | public static void enable() { 59 | triggerHandlerDisabled = false; 60 | } 61 | } -------------------------------------------------------------------------------- /src/classes/ObjectMergePairTriggerHandler.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ObjectMergePairTriggerHandlerTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | @isTest 33 | private class ObjectMergePairTriggerHandlerTest { 34 | 35 | @isTest 36 | static void test_move_victim_keep_master() { 37 | 38 | Object_Merge_Handler__c h2 = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Contact' AND RecordType.DeveloperName = 'Child_Handler']; 39 | h2.Standard_Action__c = 'Move Victim'; 40 | h2.Merge_Action__c = 'Keep Master'; 41 | update h2; 42 | 43 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 44 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 45 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 46 | Id c2Id = [SELECT Id FROM Contact WHERE LastName = 'test2'].Id; 47 | Id c3Id = [SELECT Id FROM Contact WHERE LastName = 'test3'].Id; 48 | 49 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id); 50 | 51 | Test.startTest(); 52 | 53 | insert p; 54 | update p; 55 | 56 | Test.stopTest(); 57 | 58 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 59 | System.assertEquals('Merged', p.Status__c); 60 | 61 | Account a1 = [SELECT Id, Name, Website FROM Account WHERE Id = :a1Id]; 62 | System.assertEquals('test1', a1.Name); 63 | System.assertEquals('www.test2.com', a1.Website); 64 | 65 | System.assert([SELECT Id FROM Account WHERE Id = :a2Id].isEmpty()); 66 | 67 | Contact c1 = [SELECT Id, AccountId, LastName, Email, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE Id = :c1Id]; 68 | System.assertEquals(a1Id, c1.AccountId); 69 | System.assertEquals('test1', c1.LastName); 70 | System.assertEquals('test@test.com', c1.Email); 71 | System.assertEquals(false, c1.DoNotCall); 72 | System.assertEquals(true, c1.HasOptedOutOfEmail); 73 | 74 | System.assert([SELECT Id FROM Contact WHERE Id = :c2Id].isEmpty()); 75 | 76 | Contact c3 = [SELECT Id, AccountId FROM Contact WHERE Id = :c3Id]; 77 | System.assertEquals(a1Id, c3.AccountId); 78 | } 79 | 80 | @isTest 81 | static void test_delete_victim_delete_duplicate() { 82 | 83 | Object_Merge_Handler__c h2 = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Contact' AND RecordType.DeveloperName = 'Child_Handler']; 84 | h2.Standard_Action__c = 'Delete Victim'; 85 | h2.Merge_Action__c = 'Delete Duplicate'; 86 | update h2; 87 | 88 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 89 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 90 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 91 | Id c2Id = [SELECT Id FROM Contact WHERE LastName = 'test2'].Id; 92 | Id c3Id = [SELECT Id FROM Contact WHERE LastName = 'test3'].Id; 93 | 94 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id); 95 | 96 | Test.startTest(); 97 | 98 | insert p; 99 | 100 | Test.stopTest(); 101 | 102 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 103 | System.assertEquals('Merged', p.Status__c); 104 | 105 | Account a1 = [SELECT Id, Name, Website FROM Account WHERE Id = :a1Id]; 106 | System.assertEquals('test1', a1.Name); 107 | System.assertEquals('www.test2.com', a1.Website); 108 | 109 | System.assert([SELECT Id FROM Account WHERE Id = :a2Id].isEmpty()); 110 | 111 | Contact c1 = [SELECT Id, AccountId, LastName, Email, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE Id = :c1Id]; 112 | System.assertEquals(a1Id, c1.AccountId); 113 | System.assertEquals('test1', c1.LastName); 114 | System.assertEquals(null, c1.Email); 115 | System.assertEquals(false, c1.DoNotCall); 116 | System.assertEquals(false, c1.HasOptedOutOfEmail); 117 | 118 | System.assert([SELECT Id FROM Contact WHERE Id = :c2Id].isEmpty()); 119 | 120 | System.assert([SELECT Id FROM Contact WHERE Id = :c3Id].isEmpty()); 121 | } 122 | 123 | @isTest 124 | static void test_clone_victim_clone_reparented_victim() { 125 | 126 | Object_Merge_Handler__c h2 = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Contact' AND RecordType.DeveloperName = 'Child_Handler']; 127 | h2.Standard_Action__c = 'Clone Victim'; 128 | h2.Merge_Action__c = 'Keep Oldest Created'; 129 | h2.Clone_Reparented_Victim__c = true; 130 | update h2; 131 | 132 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 133 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 134 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 135 | Id c2Id = [SELECT Id FROM Contact WHERE LastName = 'test2'].Id; 136 | Id c3Id = [SELECT Id FROM Contact WHERE LastName = 'test3'].Id; 137 | 138 | Test.setCreatedDate(c1Id, System.now().addHours(-1)); 139 | Test.setCreatedDate(c2Id, System.now().addHours(-2)); 140 | 141 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id); 142 | 143 | Test.startTest(); 144 | 145 | insert p; 146 | 147 | Test.stopTest(); 148 | 149 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 150 | System.assertEquals('Merged', p.Status__c); 151 | 152 | Account a1 = [SELECT Id, Name, Website FROM Account WHERE Id = :a1Id]; 153 | System.assertEquals('test1', a1.Name); 154 | System.assertEquals('www.test2.com', a1.Website); 155 | 156 | System.assert([SELECT Id FROM Account WHERE Id = :a2Id].isEmpty()); 157 | 158 | System.assert([SELECT Id FROM Contact WHERE Id = :c1Id].isEmpty()); 159 | System.assert([SELECT Id FROM Contact WHERE Id = :c2Id].isEmpty()); 160 | System.assert([SELECT Id FROM Contact WHERE Id = :c3Id].isEmpty()); 161 | 162 | Contact clone = [SELECT Id, AccountId, LastName, Email, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE LastName = 'test2']; 163 | System.assertEquals(a1Id, clone.AccountId); 164 | System.assertEquals('test@test.com', clone.Email); 165 | System.assertEquals(true, clone.DoNotCall); 166 | System.assertEquals(true, clone.HasOptedOutOfEmail); 167 | 168 | Contact c3New = [SELECT Id, AccountId FROM Contact WHERE LastName = 'test3']; 169 | System.assertEquals(a1Id, c3New.AccountId); 170 | } 171 | 172 | @isTest 173 | static void test_keep_oldest_created() { 174 | 175 | Object_Merge_Handler__c h2 = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Contact' AND RecordType.DeveloperName = 'Child_Handler']; 176 | h2.Merge_Action__c = 'Keep Oldest Created'; 177 | update h2; 178 | 179 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 180 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 181 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 182 | Id c2Id = [SELECT Id FROM Contact WHERE LastName = 'test2'].Id; 183 | 184 | Test.setCreatedDate(c1Id, System.now().addHours(-1)); 185 | Test.setCreatedDate(c2Id, System.now().addHours(-2)); 186 | 187 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id); 188 | 189 | Test.startTest(); 190 | 191 | insert p; 192 | 193 | Test.stopTest(); 194 | 195 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 196 | System.assertEquals('Merged', p.Status__c); 197 | 198 | Account a1 = [SELECT Id, Name, Website FROM Account WHERE Id = :a1Id]; 199 | System.assertEquals('test1', a1.Name); 200 | System.assertEquals('www.test2.com', a1.Website); 201 | 202 | System.assert([SELECT Id FROM Account WHERE Id = :a2Id].isEmpty()); 203 | 204 | System.assert([SELECT Id FROM Contact WHERE Id = :c1Id].isEmpty()); 205 | 206 | Contact c2 = [SELECT Id, AccountId, LastName, Email, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE Id = :c2Id]; 207 | System.assertEquals(a1Id, c2.AccountId); 208 | System.assertEquals('test2', c2.LastName); 209 | System.assertEquals('test@test.com', c2.Email); 210 | System.assertEquals(true, c2.DoNotCall); 211 | System.assertEquals(true, c2.HasOptedOutOfEmail); 212 | } 213 | 214 | @isTest 215 | static void test_keep_newest_created() { 216 | 217 | Object_Merge_Handler__c h2 = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Contact' AND RecordType.DeveloperName = 'Child_Handler']; 218 | h2.Merge_Action__c = 'Keep Newest Created'; 219 | update h2; 220 | 221 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 222 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 223 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 224 | Id c2Id = [SELECT Id FROM Contact WHERE LastName = 'test2'].Id; 225 | 226 | Test.setCreatedDate(c1Id, System.now().addHours(-1)); 227 | Test.setCreatedDate(c2Id, System.now().addHours(-2)); 228 | 229 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id); 230 | 231 | Test.startTest(); 232 | 233 | insert p; 234 | 235 | Test.stopTest(); 236 | 237 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 238 | System.assertEquals('Merged', p.Status__c); 239 | 240 | Account a1 = [SELECT Id, Name, Website FROM Account WHERE Id = :a1Id]; 241 | System.assertEquals('test1', a1.Name); 242 | System.assertEquals('www.test2.com', a1.Website); 243 | 244 | System.assert([SELECT Id FROM Account WHERE Id = :a2Id].isEmpty()); 245 | 246 | Contact c1 = [SELECT Id, AccountId, LastName, Email, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE Id = :c1Id]; 247 | System.assertEquals(a1Id, c1.AccountId); 248 | System.assertEquals('test1', c1.LastName); 249 | System.assertEquals('test@test.com', c1.Email); 250 | System.assertEquals(false, c1.DoNotCall); 251 | System.assertEquals(true, c1.HasOptedOutOfEmail); 252 | 253 | System.assert([SELECT Id FROM Contact WHERE Id = :c2Id].isEmpty()); 254 | } 255 | 256 | @isTest 257 | static void test_keep_last_modified() { 258 | 259 | Object_Merge_Handler__c h2 = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Contact' AND RecordType.DeveloperName = 'Child_Handler']; 260 | h2.Merge_Action__c = 'Keep Last Modified'; 261 | update h2; 262 | 263 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 264 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 265 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 266 | Id c2Id = [SELECT Id FROM Contact WHERE LastName = 'test2'].Id; 267 | 268 | update (new Contact(Id = c1Id)); 269 | 270 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id); 271 | 272 | Test.startTest(); 273 | 274 | insert p; 275 | 276 | Test.stopTest(); 277 | 278 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 279 | System.assertEquals('Merged', p.Status__c); 280 | 281 | Account a1 = [SELECT Id, Name, Website FROM Account WHERE Id = :a1Id]; 282 | System.assertEquals('test1', a1.Name); 283 | System.assertEquals('www.test2.com', a1.Website); 284 | 285 | System.assert([SELECT Id FROM Account WHERE Id = :a2Id].isEmpty()); 286 | 287 | Contact c1 = [SELECT Id, AccountId, LastName, Email, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE Id = :c1Id]; 288 | System.assertEquals(a1Id, c1.AccountId); 289 | System.assertEquals('test1', c1.LastName); 290 | System.assertEquals('test@test.com', c1.Email); 291 | System.assertEquals(false, c1.DoNotCall); 292 | System.assertEquals(true, c1.HasOptedOutOfEmail); 293 | 294 | System.assert([SELECT Id FROM Contact WHERE Id = :c2Id].isEmpty()); 295 | } 296 | 297 | @isTest 298 | static void test_no_victim_children() { 299 | 300 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 301 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 302 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 303 | 304 | delete [SELECT Id FROM Contact WHERE AccountId = :a2Id]; 305 | 306 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = a2Id); 307 | 308 | Test.startTest(); 309 | 310 | insert p; 311 | 312 | Test.stopTest(); 313 | 314 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 315 | System.assertEquals('Merged', p.Status__c); 316 | 317 | Account a1 = [SELECT Id, Name, Website FROM Account WHERE Id = :a1Id]; 318 | System.assertEquals('test1', a1.Name); 319 | System.assertEquals('www.test2.com', a1.Website); 320 | 321 | System.assert([SELECT Id FROM Account WHERE Id = :a2Id].isEmpty()); 322 | 323 | Contact c1 = [SELECT Id, AccountId, LastName, Email, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE Id = :c1Id]; 324 | System.assertEquals(a1Id, c1.AccountId); 325 | System.assertEquals('test1', c1.LastName); 326 | System.assertEquals(null, c1.Email); 327 | System.assertEquals(false, c1.DoNotCall); 328 | System.assertEquals(false, c1.HasOptedOutOfEmail); 329 | } 330 | 331 | @isTest 332 | static void test_keep_least_recent_value() { 333 | 334 | Id pId = Schema.SObjectType.Object_Merge_Handler__c.getRecordTypeInfosByDeveloperName().get('Parent_Handler').getRecordTypeId(); 335 | 336 | Object_Merge_Handler__c h1 = new Object_Merge_Handler__c(Name = 'Account', Active__c = true, RecordTypeId = pId); 337 | Object_Merge_Handler__c h2 = new Object_Merge_Handler__c(Name = 'Account', Active__c = true, RecordTypeId = pId); 338 | insert new List{h1, h2}; 339 | 340 | Object_Merge_Field__c f1 = new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = h1.Id, Active__c = true); 341 | Object_Merge_Field__c f2 = new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = h2.Id, Active__c = true); 342 | insert new List{f1, f2}; 343 | 344 | Datetime createdDate = System.now().addHours(-1); 345 | 346 | Test.setCreatedDate(h1.Id, createdDate); 347 | Test.setCreatedDate(h2.Id, createdDate); 348 | Test.setCreatedDate(f1.Id, createdDate); 349 | Test.setCreatedDate(f2.Id, createdDate); 350 | 351 | h2.Active__c = false; 352 | update h2; 353 | 354 | f2.Active__c = false; 355 | update f2; 356 | 357 | insert new Object_Merge_Handler__History(ParentId = h2.Id, Field = 'Active__c'); 358 | insert new Object_Merge_Field__History(ParentId = f2.Id, Field = 'Active__c'); 359 | 360 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = h1.Id, Victim_ID__c = h2.Id); 361 | 362 | Test.startTest(); 363 | 364 | insert p; 365 | 366 | Test.stopTest(); 367 | 368 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 369 | System.assertEquals('Merged', p.Status__c); 370 | 371 | h1 = [SELECT Id, Active__c FROM Object_Merge_Handler__c WHERE Id = :h1.Id]; 372 | System.assertEquals(true, h1.Active__c); 373 | 374 | System.assert([SELECT Id FROM Object_Merge_Handler__c WHERE Id = :h2.Id].isEmpty()); 375 | 376 | f1 = [SELECT Id, Active__c FROM Object_Merge_Field__c WHERE Id = :f1.Id]; 377 | System.assertEquals(true, f1.Active__c); 378 | 379 | System.assert([SELECT Id FROM Object_Merge_Field__c WHERE Id = :f2.Id].isEmpty()); 380 | } 381 | 382 | @isTest 383 | static void test_keep_most_recent_value() { 384 | 385 | Object_Merge_Field__c f8 = [SELECT Id FROM Object_Merge_Field__c WHERE Object_Merge_Handler__r.Name = 'Object_Merge_Handler__c' AND Name = 'Active__c']; 386 | Object_Merge_Field__c f9 = [SELECT Id FROM Object_Merge_Field__c WHERE Object_Merge_Handler__r.Name = 'Object_Merge_Field__c' AND Name = 'Active__c']; 387 | 388 | f8.Keep_Least_Recent_Value__c = false; 389 | f9.Keep_Least_Recent_Value__c = false; 390 | f8.Keep_Most_Recent_Value__c = true; 391 | f9.Keep_Most_Recent_Value__c = true; 392 | 393 | update new List{f8, f9}; 394 | 395 | Id pId = Schema.SObjectType.Object_Merge_Handler__c.getRecordTypeInfosByDeveloperName().get('Parent_Handler').getRecordTypeId(); 396 | 397 | Object_Merge_Handler__c h1 = new Object_Merge_Handler__c(Name = 'Account', Active__c = true, RecordTypeId = pId); 398 | Object_Merge_Handler__c h2 = new Object_Merge_Handler__c(Name = 'Account', Active__c = true, RecordTypeId = pId); 399 | insert new List{h1, h2}; 400 | 401 | Object_Merge_Field__c f1 = new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = h1.Id, Active__c = true); 402 | Object_Merge_Field__c f2 = new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = h2.Id, Active__c = true); 403 | insert new List{f1, f2}; 404 | 405 | Datetime createdDate = System.now().addHours(-1); 406 | 407 | Test.setCreatedDate(h1.Id, createdDate); 408 | Test.setCreatedDate(h2.Id, createdDate); 409 | Test.setCreatedDate(f1.Id, createdDate); 410 | Test.setCreatedDate(f2.Id, createdDate); 411 | 412 | h2.Active__c = false; 413 | update h2; 414 | 415 | f2.Active__c = false; 416 | update f2; 417 | 418 | insert new Object_Merge_Handler__History(ParentId = h2.Id, Field = 'Active__c'); 419 | insert new Object_Merge_Field__History(ParentId = f2.Id, Field = 'Active__c'); 420 | 421 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = h1.Id, Victim_ID__c = h2.Id); 422 | 423 | Test.startTest(); 424 | 425 | insert p; 426 | 427 | Test.stopTest(); 428 | 429 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 430 | System.assertEquals('Merged', p.Status__c); 431 | 432 | h1 = [SELECT Id, Active__c FROM Object_Merge_Handler__c WHERE Id = :h1.Id]; 433 | System.assertEquals(false, h1.Active__c); 434 | 435 | System.assert([SELECT Id FROM Object_Merge_Handler__c WHERE Id = :h2.Id].isEmpty()); 436 | 437 | f1 = [SELECT Id, Active__c FROM Object_Merge_Field__c WHERE Id = :f1.Id]; 438 | System.assertEquals(false, f1.Active__c); 439 | 440 | System.assert([SELECT Id FROM Object_Merge_Field__c WHERE Id = :f2.Id].isEmpty()); 441 | } 442 | 443 | @isTest 444 | static void test_handle_users_1() { 445 | 446 | Id c4Id = [SELECT Id FROM Contact WHERE LastName = 'test4'].Id; 447 | Id c5Id = [SELECT Id FROM Contact WHERE LastName = 'test5'].Id; 448 | 449 | Boolean testUserMerge = ObjectMergeUtility.portalEnabled && Database.countQuery('SELECT count() FROM User WHERE ContactId = :c4Id') == 1; 450 | 451 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = c4Id, Victim_ID__c = c5Id); 452 | 453 | Test.startTest(); 454 | 455 | insert p; 456 | 457 | Test.stopTest(); 458 | 459 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 460 | System.assertEquals(testUserMerge ? 'Processing' : 'Merged', p.Status__c); 461 | } 462 | 463 | @isTest 464 | static void test_handle_users_2() { 465 | 466 | Id c4Id = [SELECT Id FROM Contact WHERE LastName = 'test4'].Id; 467 | Id c5Id = [SELECT Id FROM Contact WHERE LastName = 'test5'].Id; 468 | 469 | Boolean testUserMerge = ObjectMergeUtility.portalEnabled && Database.countQuery('SELECT count() FROM User WHERE ContactId = :c4Id') == 1; 470 | 471 | Set userIdsToDeactivate = new Set(); 472 | Map ownerIdMap = new Map(); 473 | if (testUserMerge) { 474 | 475 | Id u4Id = Database.query('SELECT Id FROM User WHERE ContactId = :c4Id')[0].Id; 476 | Id u5Id = Database.query('SELECT Id FROM User WHERE ContactId = :c5Id')[0].Id; 477 | 478 | userIdsToDeactivate.add(u5Id); 479 | ownerIdMap.put(u5Id, u4Id); 480 | 481 | } else { 482 | userIdsToDeactivate.add(UserInfo.getUserId()); 483 | } 484 | 485 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = c4Id, Victim_ID__c = c5Id); 486 | 487 | ObjectMergePairTriggerHandler.disable(); 488 | insert p; 489 | ObjectMergePairTriggerHandler.enable(); 490 | 491 | Test.startTest(); 492 | 493 | ObjectMergeHandleUsers cls = new ObjectMergeHandleUsers(new Set{p.Id}, userIdsToDeactivate, ownerIdMap); 494 | cls.execute(true); 495 | 496 | Test.stopTest(); 497 | } 498 | 499 | @isTest 500 | static void test_handle_users_3() { 501 | 502 | Id c4Id = [SELECT Id FROM Contact WHERE LastName = 'test4'].Id; 503 | Id c6Id = [SELECT Id FROM Contact WHERE LastName = 'test6'].Id; 504 | 505 | Boolean testUserMerge = ObjectMergeUtility.portalEnabled && Database.countQuery('SELECT count() FROM User WHERE ContactId = :c4Id') == 1; 506 | 507 | Id u4Id; 508 | Map ownerIdMap = new Map(); 509 | 510 | if (testUserMerge) { 511 | 512 | u4Id = Database.query('SELECT Id FROM User WHERE ContactId = :c4Id')[0].Id; 513 | ownerIdMap.put(UserInfo.getUserId(), u4Id); 514 | } 515 | 516 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = c4Id, Victim_ID__c = c6Id); 517 | 518 | ObjectMergePairTriggerHandler.disable(); 519 | insert p; 520 | ObjectMergePairTriggerHandler.enable(); 521 | 522 | Test.startTest(); 523 | 524 | System.enqueueJob(new ObjectMergeHandleUsers(new Set{p.Id}, new Set(), ownerIdMap)); 525 | 526 | Test.stopTest(); 527 | 528 | Contact c4 = [SELECT Id, LastName, (SELECT Id, OwnerId FROM Tasks) FROM Contact WHERE Id = :c4Id]; 529 | System.assertEquals('test4', c4.LastName); 530 | 531 | System.assert([SELECT Id FROM Contact WHERE Id = :c6Id].isEmpty()); 532 | 533 | if (testUserMerge) { 534 | 535 | System.assertEquals(1, c4.Tasks.size()); 536 | System.assertEquals(u4Id, c4.Tasks[0].OwnerId); 537 | } 538 | 539 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 540 | System.assertEquals('Merged', p.Status__c); 541 | } 542 | 543 | @isTest 544 | static void test_handle_users_4() { 545 | 546 | Id c4Id = [SELECT Id FROM Contact WHERE LastName = 'test4'].Id; 547 | Id c5Id = [SELECT Id FROM Contact WHERE LastName = 'test5'].Id; 548 | 549 | Boolean testUserMerge = ObjectMergeUtility.portalEnabled && Database.countQuery('SELECT count() FROM User WHERE ContactId = :c4Id') == 1; 550 | 551 | Object_Merge_Pair__c p = new Object_Merge_Pair__c(Master_ID__c = c4Id, Victim_ID__c = c5Id, Status__c = 'Processing'); 552 | 553 | ObjectMergePairTriggerHandler.disable(); 554 | insert p; 555 | ObjectMergePairTriggerHandler.enable(); 556 | 557 | Test.startTest(); 558 | 559 | ObjectMergeHandleUsers.handleUsers(new List{p}); 560 | 561 | if (!ObjectMergeUtility.portalEnabled) 562 | ObjectMergeHandleUsers.setUserFieldsToDeactivate(new List{new User()}); 563 | 564 | Test.stopTest(); 565 | 566 | p = [SELECT Id, Status__c FROM Object_Merge_Pair__c WHERE Id = :p.Id]; 567 | System.assertEquals(testUserMerge ? 'Processing' : 'Merged', p.Status__c); 568 | } 569 | 570 | @isTest 571 | static void test_errors_1() { 572 | 573 | Id a1Id = [SELECT Id FROM Account WHERE Name = 'test1'].Id; 574 | Id a2Id = [SELECT Id FROM Account WHERE Name = 'test2'].Id; 575 | Id c1Id = [SELECT Id FROM Contact WHERE LastName = 'test1'].Id; 576 | 577 | Object_Merge_Pair__c p1 = new Object_Merge_Pair__c(Status__c = 'Retry', Master_ID__c = a1Id, Victim_ID__c = a2Id); 578 | Object_Merge_Pair__c p2 = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = c1Id); 579 | Object_Merge_Pair__c p3 = new Object_Merge_Pair__c(Master_ID__c = '00T0H00003uKOXt', Victim_ID__c = '00T0H00003uKOXt'); 580 | Object_Merge_Pair__c p4 = new Object_Merge_Pair__c(Master_ID__c = a1Id, Victim_ID__c = '0011900000xbYvA'); 581 | 582 | Test.startTest(); 583 | 584 | insert new List{p1, p2, p3, p4}; 585 | 586 | Test.stopTest(); 587 | 588 | p1 = [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :p1.Id]; 589 | p2 = [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :p2.Id]; 590 | p3 = [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :p3.Id]; 591 | p4 = [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :p4.Id]; 592 | 593 | System.assertEquals('Error', p1.Status__c); 594 | System.assertEquals('Error', p2.Status__c); 595 | System.assertEquals('Error', p3.Status__c); 596 | System.assertEquals('Error', p4.Status__c); 597 | 598 | System.assertEquals('Invalid status', p1.Error_Reason__c); 599 | System.assertEquals('Invalid Master/Victim ID pair', p2.Error_Reason__c); 600 | System.assertEquals('Object Merge Handler not found', p3.Error_Reason__c); 601 | System.assertEquals('Master and/or victim not found', p4.Error_Reason__c); 602 | } 603 | 604 | @isTest 605 | static void test_errors_2() { 606 | 607 | Object_Merge_Field__c activeField = [SELECT Id FROM Object_Merge_Field__c WHERE Object_Merge_Handler__r.Name = 'Object_Merge_Handler__c' AND Name = 'Active__c']; 608 | 609 | activeField.Treat_False_as_Null__c = true; 610 | 611 | update activeField; 612 | 613 | Id parentRtId = Schema.SObjectType.Object_Merge_Handler__c.getRecordTypeInfosByDeveloperName().get('Parent_Handler').getRecordTypeId(); 614 | 615 | Object_Merge_Handler__c h1 = new Object_Merge_Handler__c(Name = 'not_an_object', Active__c = false, RecordTypeId = parentRtId); 616 | Object_Merge_Handler__c h2 = new Object_Merge_Handler__c(Name = 'Task', Active__c = true, RecordTypeId = parentRtId); 617 | 618 | insert new List{h1, h2}; 619 | 620 | Object_Merge_Pair__c p1 = new Object_Merge_Pair__c(Master_ID__c = 'test', Victim_ID__c = 'test'); 621 | Object_Merge_Pair__c p2 = new Object_Merge_Pair__c(Master_ID__c = 'test', Victim_ID__c = 'test'); 622 | Object_Merge_Pair__c p3 = new Object_Merge_Pair__c(Master_ID__c = h1.Id, Victim_ID__c = h2.Id); 623 | 624 | insert new List{p1, p2}; 625 | 626 | p1.Status__c = 'Retry'; 627 | p1.Master_ID__c = p2.Id; 628 | p1.Victim_ID__c = p1.Id; 629 | 630 | Test.startTest(); 631 | 632 | update p1; 633 | insert p3; 634 | 635 | Test.stopTest(); 636 | 637 | p1 = [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :p1.Id]; 638 | p3 = [SELECT Id, Status__c, Error_Reason__c FROM Object_Merge_Pair__c WHERE Id = :p3.Id]; 639 | 640 | System.assertEquals('Error', p1.Status__c); 641 | System.assertEquals('Error performing DML', p1.Error_Reason__c); 642 | 643 | System.assertEquals('Error', p3.Status__c); 644 | System.assertEquals('Error performing DML', p3.Error_Reason__c); 645 | } 646 | 647 | @testSetup 648 | static void setup() { 649 | 650 | Id portalAccountOwnerId = UserInfo.getUserId(); 651 | Boolean userRoleFound = UserInfo.getUserRoleId() != null; 652 | if (!userRoleFound) { 653 | for (User u:[SELECT Id FROM User WHERE IsActive = true AND UserRoleId != null AND UserType = 'Standard' LIMIT 1]) { 654 | userRoleFound = true; 655 | portalAccountOwnerId = u.Id; 656 | } 657 | } 658 | 659 | Account a1 = new Account(Name = 'test1'); 660 | Account a2 = new Account(Name = 'test2', Website = 'www.test2.com'); 661 | Account a3 = new Account(Name = 'test3', OwnerId = portalAccountOwnerId); 662 | Account a4 = new Account(Name = 'test4', OwnerId = portalAccountOwnerId); 663 | Account a5 = new Account(Name = 'test5', OwnerId = portalAccountOwnerId); 664 | insert new List{a1, a2, a3, a4, a5}; 665 | 666 | Contact c1 = new Contact(AccountId = a1.Id, LastName = 'test1', FirstName = 'test'); 667 | Contact c2 = new Contact(AccountId = a2.Id, LastName = 'test2', FirstName = 'test', Email = 'test@test.com', DoNotCall = true, HasOptedOutOfEmail = true); 668 | Contact c3 = new Contact(AccountId = a2.Id, LastName = 'test3', FirstName = 'not_test'); 669 | Contact c4 = new Contact(AccountId = a3.Id, LastName = 'test4', FirstName = 'test'); 670 | Contact c5 = new Contact(AccountId = a4.Id, LastName = 'test5', FirstName = 'test'); 671 | Contact c6 = new Contact(AccountId = a5.Id, LastName = 'test6', FirstName = 'test'); 672 | insert new List{c1, c2, c3, c4, c5, c6}; 673 | 674 | if (userRoleFound && ObjectMergeUtility.portalEnabled) { 675 | 676 | Datetime orgCreatedDate = [SELECT Id, CreatedDate FROM Organization LIMIT 1].CreatedDate.addHours(1); 677 | 678 | Id profileId; 679 | for (Profile p:[SELECT Id FROM Profile WHERE UserType IN ('CSPLitePortal', 'CustomerSuccess', 'PowerCustomerSuccess') AND CreatedDate > :orgCreatedDate ORDER BY CreatedDate DESC LIMIT 1]) 680 | profileId = p.Id; 681 | 682 | if (profileId != null) { 683 | 684 | User u4 = new User(Username = 'test_object_merge_4@test.com', ProfileId = profileId, Alias = 'test4', Email = 'test@test.com', EmailEncodingKey = 'UTF-8', FirstName = 'test4', LastName = 'test4', IsActive = true, TimeZoneSidKey = 'America/Chicago', LocaleSidKey = 'en_US', LanguageLocaleKey = 'en_US'); 685 | User u5 = new User(Username = 'test_object_merge_5@test.com', ProfileId = profileId, Alias = 'test5', Email = 'test@test.com', EmailEncodingKey = 'UTF-8', FirstName = 'test5', LastName = 'test5', IsActive = true, TimeZoneSidKey = 'America/Chicago', LocaleSidKey = 'en_US', LanguageLocaleKey = 'en_US'); 686 | 687 | u4.put('ContactId', c4.Id); 688 | u4.put('CommunityNickname', 'test4'); 689 | 690 | u5.put('ContactId', c5.Id); 691 | u5.put('CommunityNickname', 'test5'); 692 | 693 | insert new List{u4, u5}; 694 | 695 | insert (new Task(WhoId = c6.Id, OwnerId = UserInfo.getUserId())); 696 | } 697 | } 698 | 699 | Id pId = Schema.SObjectType.Object_Merge_Handler__c.getRecordTypeInfosByDeveloperName().get('Parent_Handler').getRecordTypeId(); 700 | Id cId = Schema.SObjectType.Object_Merge_Handler__c.getRecordTypeInfosByDeveloperName().get('Child_Handler').getRecordTypeId(); 701 | 702 | Object_Merge_Handler__c ph1 = new Object_Merge_Handler__c(Name = 'Account', Active__c = true, RecordTypeId = pId); 703 | Object_Merge_Handler__c ph2 = new Object_Merge_Handler__c(Name = 'Contact', Active__c = true, RecordTypeId = pId); 704 | Object_Merge_Handler__c ph3 = new Object_Merge_Handler__c(Name = 'Object_Merge_Handler__c', RecordTypeId = pId); 705 | Object_Merge_Handler__c ph4 = new Object_Merge_Handler__c(Name = 'Object_Merge_Pair__c', RecordTypeId = pId); 706 | insert new List{ph1, ph2, ph3, ph4}; 707 | 708 | Object_Merge_Handler__c ch1 = new Object_Merge_Handler__c(Name = 'Contact', Parent_Handler__c = ph1.Id, Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Delete Victim', Active__c = true, RecordTypeId = cId); 709 | Object_Merge_Handler__c ch2 = new Object_Merge_Handler__c(Name = 'Task', Parent_Handler__c = ph2.Id, Object_Lookup_Field_API_Name__c = 'WhoId', Standard_Action__c = 'Move Victim', Active__c = true, RecordTypeId = cId); 710 | Object_Merge_Handler__c ch3 = new Object_Merge_Handler__c(Name = 'Object_Merge_Field__c', Parent_Handler__c = ph3.Id, Object_Lookup_Field_API_Name__c = 'Object_Merge_Handler__c', Standard_Action__c = 'Move Victim', Merge_Action__c = 'Keep Master', Active__c = true, RecordTypeId = cId); 711 | 712 | List childHandlers = new List{ch1, ch2, ch3}; 713 | 714 | if (ObjectMergeUtility.portalEnabled) 715 | childHandlers.add(new Object_Merge_Handler__c(Name = 'User', Parent_Handler__c = ph2.Id, Object_Lookup_Field_API_Name__c = 'ContactId', Standard_Action__c = 'Delete Victim', Active__c = true, RecordTypeId = cId)); 716 | 717 | insert childHandlers; 718 | 719 | Object_Merge_Field__c f1 = new Object_Merge_Field__c(Name = 'Name', Use_for_Matching__c = false, Object_Merge_Handler__c = ph1.Id, Active__c = true); 720 | Object_Merge_Field__c f2 = new Object_Merge_Field__c(Name = 'Website', Use_for_Matching__c = false, Object_Merge_Handler__c = ph1.Id, Active__c = true); 721 | Object_Merge_Field__c f3 = new Object_Merge_Field__c(Name = 'LastName', Use_for_Matching__c = false, Object_Merge_Handler__c = ch1.Id, Active__c = true); 722 | Object_Merge_Field__c f4 = new Object_Merge_Field__c(Name = 'FirstName', Use_for_Matching__c = true, Object_Merge_Handler__c = ch1.Id, Active__c = true); 723 | Object_Merge_Field__c f5 = new Object_Merge_Field__c(Name = 'Email', Use_for_Matching__c = false, Object_Merge_Handler__c = ch1.Id, Active__c = true); 724 | Object_Merge_Field__c f6 = new Object_Merge_Field__c(Name = 'DoNotCall', Use_for_Matching__c = false, Object_Merge_Handler__c = ch1.Id, Active__c = true); 725 | Object_Merge_Field__c f7 = new Object_Merge_Field__c(Name = 'HasOptedOutOfEmail', Use_for_Matching__c = false, Object_Merge_Handler__c = ch1.Id, Active__c = true, Treat_False_as_Null__c = true); 726 | Object_Merge_Field__c f8 = new Object_Merge_Field__c(Name = 'Active__c', Use_for_Matching__c = false, Object_Merge_Handler__c = ph3.Id, Active__c = true, Keep_Least_Recent_Value__c = true); 727 | Object_Merge_Field__c f9 = new Object_Merge_Field__c(Name = 'Name', Use_for_Matching__c = true, Object_Merge_Handler__c = ch3.Id, Active__c = true); 728 | Object_Merge_Field__c f10 = new Object_Merge_Field__c(Name = 'Active__c', Use_for_Matching__c = false, Object_Merge_Handler__c = ch3.Id, Active__c = true, Keep_Least_Recent_Value__c = true); 729 | insert new List{f1, f2, f3, f4, f5, f6, f7, f8, f9, f10}; 730 | } 731 | } -------------------------------------------------------------------------------- /src/classes/ObjectMergePairTriggerHandlerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ObjectMergeUtility.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ObjectMergeValidator.cls: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | public class ObjectMergeValidator { 33 | 34 | // Get global describe once to save CPU time 35 | private static final Map GLOBAL_DESCRIBE = Schema.getGlobalDescribe(); 36 | 37 | // Record Type ID for Child Object Merge Handlers 38 | private static final Id CHILD_HANDLER_RT_ID = Schema.SObjectType.Object_Merge_Handler__c.getRecordTypeInfosByDeveloperName().get('Child_Handler').getRecordTypeId(); 39 | 40 | // Error messages 41 | private static final String INVALID_OBJECT_ERROR = 'This Object Merge Handler is invalid because the SObject does not exist. Check to make sure the API name is valid.'; 42 | private static final String INVALID_LOOKUP_FIELD_ERROR = 'This Object Merge Handler is invalid because the Object Lookup Field does not exist on the object. Check to make sure the API name is valid.'; 43 | private static final String INVALID_LOOKUP_ERROR = 'This Object Merge Handler is invalid because the Object Lookup Field is not a field that references the parent object.'; 44 | private static final String INVALID_FIELD_ERROR = 'This Object Merge Field is invalid because the field does not exist on the parent object. Check to make sure the API name is valid.'; 45 | private static final String INVALID_KEEP_MOST_RECENT_VALUE_FIELD_ERROR = 'This Object Merge Field is invalid because the Keep Most Recent Value checkbox is checked but Field History Tracking is not enabled.'; 46 | 47 | // Method to ensure the Object Merge Handlers are valid 48 | public static void validateObjectMergeHandlers(List objectMergeHandlers) { 49 | 50 | Set parentHandlerIds = new Set(); // Set of parent object merge handler IDs from child handlers 51 | List childHandlers = new List(); // Set of child object merge handlers 52 | 53 | // Loop through all handlers and add error to any active handlers 54 | // that have an invalid object API name or invalid lookup field API name 55 | for (Object_Merge_Handler__c h:objectMergeHandlers) { 56 | 57 | // Only run on active handlers 58 | if (h.Active__c) { 59 | 60 | // Check if object API name is valid and add error if not 61 | if (!GLOBAL_DESCRIBE.containsKey(h.Name)) { 62 | 63 | h.addError(INVALID_OBJECT_ERROR); // Add error to handler 64 | 65 | } else if (h.RecordTypeId == CHILD_HANDLER_RT_ID && h.Parent_Handler__c != null) { 66 | 67 | // Check if lookup field API name is valid and add error if not 68 | if (!GLOBAL_DESCRIBE.get(h.Name).getDescribe().fields.getMap().containsKey(h.Object_Lookup_Field_API_Name__c)) { 69 | 70 | h.addError(INVALID_LOOKUP_FIELD_ERROR); // Add error to handler 71 | 72 | } else { 73 | 74 | parentHandlerIds.add(h.Parent_Handler__c); // Add parent handler ID to set 75 | childHandlers.add(h); // Add handler to list of child handlers 76 | } 77 | } 78 | } 79 | } 80 | 81 | // Ensure all child handlers have valid lookup field 82 | if (!childHandlers.isEmpty()) { 83 | 84 | // Get map of parent handlers 85 | Map parentHandlers = new Map([SELECT Id, Name FROM Object_Merge_Handler__c WHERE Id IN :parentHandlerIds]); 86 | 87 | // Loop over child handlers and ensure lookup field references parent object type 88 | for (Object_Merge_Handler__c h:childHandlers) { 89 | 90 | // Get object type of parent handler 91 | Schema.SObjectType parentObject = GLOBAL_DESCRIBE.get(parentHandlers.get(h.Parent_Handler__c).Name); 92 | 93 | // Get list of object types the lookup field references 94 | List referenceObjects = GLOBAL_DESCRIBE.get(h.Name).getDescribe().fields.getMap().get(h.Object_Lookup_Field_API_Name__c).getDescribe().getReferenceTo(); 95 | 96 | // Add error if parent object type is null, list of object types referenced is null, 97 | // or list of object types references does not contain parent object type 98 | if (parentObject == null || referenceObjects == null || !(new Set(referenceObjects)).contains(parentObject)) 99 | h.addError(INVALID_LOOKUP_ERROR); 100 | } 101 | } 102 | } 103 | 104 | // Method to ensure the Object Merge Fields are valid 105 | public static void validateObjectMergeFields(List objectMergeFields) { 106 | 107 | Set handlerIds = new Set(); // Set of handler IDs for active Object Merge Fields 108 | List activeFields = new List(); // List of active Object Merge Fields 109 | 110 | // Loop over list and get list of active Object Merge Fields and set of handler ids for those fields 111 | for (Object_Merge_Field__c f:objectMergeFields) { 112 | 113 | // Only run on active fields 114 | if (f.Active__c) { 115 | 116 | handlerIds.add(f.Object_Merge_Handler__c); // Add handler ID to set 117 | activeFields.add(f); // Add field to list 118 | } 119 | } 120 | 121 | // Ensure each active field is valid 122 | if (!activeFields.isEmpty()) { 123 | 124 | Map> handlerFieldMap = new Map>(); // Map of fields for object by Object Merge Handler ID 125 | 126 | // Query Object Merge Handlers and populate map from describe information 127 | for (Object_Merge_Handler__c h:[SELECT Id, Name FROM Object_Merge_Handler__c WHERE Id IN :handlerIds]) 128 | if (GLOBAL_DESCRIBE.containsKey(h.Name)) 129 | handlerFieldMap.put(h.Id, GLOBAL_DESCRIBE.get(h.Name).getDescribe().fields.getMap()); 130 | 131 | // Loop through all active fields, check to see if field API name is valid and add error if not 132 | for (Object_Merge_Field__c f:activeFields) 133 | if (!handlerFieldMap.containsKey(f.Object_Merge_Handler__c) || !handlerFieldMap.get(f.Object_Merge_Handler__c).containsKey(f.Name)) 134 | f.addError(INVALID_FIELD_ERROR); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/classes/ObjectMergeValidator.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ObjectMergeValidatorTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | @isTest 33 | private class ObjectMergeValidatorTest { 34 | 35 | @isTest 36 | static void test_validate_object_merge_handlers_insert() { 37 | 38 | Id parentHandlerId = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Account'].Id; 39 | Id childRtId = Object_Merge_Handler__c.SObjectType.getDescribe().getRecordTypeInfosByDeveloperName().get('Child_Handler').getRecordTypeId(); 40 | 41 | List handlers = new List(); 42 | 43 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 44 | handlers.add(new Object_Merge_Handler__c(Name = 'Not_an_Object', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 45 | handlers.add(new Object_Merge_Handler__c(Name = 'Not_an_Object', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = false, RecordTypeId = childRtId)); 46 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'Not_a_Field', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 47 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'Not_a_Field', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = false, RecordTypeId = childRtId)); 48 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'OwnerId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 49 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'OwnerId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = false, RecordTypeId = childRtId)); 50 | 51 | Test.startTest(); 52 | 53 | List results = Database.insert(handlers, false); 54 | 55 | Test.stopTest(); 56 | 57 | System.assertEquals(true, results[0].isSuccess()); 58 | System.assertEquals(false, results[1].isSuccess()); 59 | System.assertEquals(true, results[2].isSuccess()); 60 | System.assertEquals(false, results[3].isSuccess()); 61 | System.assertEquals(true, results[4].isSuccess()); 62 | System.assertEquals(false, results[5].isSuccess()); 63 | System.assertEquals(true, results[6].isSuccess()); 64 | } 65 | 66 | @isTest 67 | static void test_validate_object_merge_handlers_update() { 68 | 69 | Id parentHandlerId = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Account'].Id; 70 | Id childRtId = Object_Merge_Handler__c.SObjectType.getDescribe().getRecordTypeInfosByDeveloperName().get('Child_Handler').getRecordTypeId(); 71 | 72 | List handlers = new List(); 73 | 74 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 75 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 76 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 77 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 78 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 79 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 80 | handlers.add(new Object_Merge_Handler__c(Name = 'Contact', Object_Lookup_Field_API_Name__c = 'AccountId', Standard_Action__c = 'Move Victim', Parent_Handler__c = parentHandlerId, Active__c = true, RecordTypeId = childRtId)); 81 | 82 | insert handlers; 83 | 84 | handlers[0].Name = 'Task'; 85 | handlers[0].Object_Lookup_Field_API_Name__c = 'WhatId'; 86 | 87 | handlers[1].Name = 'Not_an_Object'; 88 | handlers[2].Name = 'Not_an_Object'; 89 | handlers[2].Active__c = false; 90 | 91 | handlers[3].Object_Lookup_Field_API_Name__c = 'Not_a_Field'; 92 | handlers[4].Object_Lookup_Field_API_Name__c = 'Not_a_Field'; 93 | handlers[4].Active__c = false; 94 | 95 | handlers[5].Object_Lookup_Field_API_Name__c = 'OwnerId'; 96 | handlers[6].Object_Lookup_Field_API_Name__c = 'OwnerId'; 97 | handlers[6].Active__c = false; 98 | 99 | Test.startTest(); 100 | 101 | List results = Database.update(handlers, false); 102 | 103 | Test.stopTest(); 104 | 105 | System.assertEquals(true, results[0].isSuccess()); 106 | System.assertEquals(false, results[1].isSuccess()); 107 | System.assertEquals(true, results[2].isSuccess()); 108 | System.assertEquals(false, results[3].isSuccess()); 109 | System.assertEquals(true, results[4].isSuccess()); 110 | System.assertEquals(false, results[5].isSuccess()); 111 | System.assertEquals(true, results[6].isSuccess()); 112 | } 113 | 114 | @isTest 115 | static void test_validate_object_merge_fields_insert() { 116 | 117 | Id parentHandlerId = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Account'].Id; 118 | 119 | List fields = new List(); 120 | 121 | fields.add(new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = parentHandlerId, Active__c = true)); 122 | fields.add(new Object_Merge_Field__c(Name = 'Not_a_Field', Object_Merge_Handler__c = parentHandlerId, Active__c = true)); 123 | fields.add(new Object_Merge_Field__c(Name = 'Not_a_Field', Object_Merge_Handler__c = parentHandlerId, Active__c = false)); 124 | 125 | Test.startTest(); 126 | 127 | List results = Database.insert(fields, false); 128 | 129 | Test.stopTest(); 130 | 131 | System.assertEquals(true, results[0].isSuccess()); 132 | System.assertEquals(false, results[1].isSuccess()); 133 | System.assertEquals(true, results[2].isSuccess()); 134 | } 135 | 136 | @isTest 137 | static void test_validate_object_merge_fields_update() { 138 | 139 | Id parentHandlerId = [SELECT Id FROM Object_Merge_Handler__c WHERE Name = 'Account'].Id; 140 | 141 | List fields = new List(); 142 | 143 | fields.add(new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = parentHandlerId, Active__c = true)); 144 | fields.add(new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = parentHandlerId, Active__c = true)); 145 | fields.add(new Object_Merge_Field__c(Name = 'Name', Object_Merge_Handler__c = parentHandlerId, Active__c = true)); 146 | 147 | insert fields; 148 | 149 | fields[0].Name = 'Website'; 150 | fields[1].Name = 'Not_a_Field'; 151 | fields[2].Name = 'Not_a_Field'; 152 | fields[2].Active__c = false; 153 | 154 | Test.startTest(); 155 | 156 | List results = Database.update(fields, false); 157 | 158 | Test.stopTest(); 159 | 160 | System.assertEquals(true, results[0].isSuccess()); 161 | System.assertEquals(false, results[1].isSuccess()); 162 | System.assertEquals(true, results[2].isSuccess()); 163 | } 164 | 165 | @testSetup 166 | static void setup() { 167 | 168 | Id parentRtId = Object_Merge_Handler__c.SObjectType.getDescribe().getRecordTypeInfosByDeveloperName().get('Parent_Handler').getRecordTypeId(); 169 | 170 | insert (new Object_Merge_Handler__c(Name = 'Account', Active__c = true, RecordTypeId = parentRtId)); 171 | } 172 | } -------------------------------------------------------------------------------- /src/classes/ObjectMergeValidatorTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/layouts/Object_Merge_Field__c-Object Merge Field Layout.layout: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeOwnerOne 4 | ChangeRecordType 5 | Submit 6 | 7 | false 8 | true 9 | true 10 | 11 | 12 | 13 | Required 14 | Object_Merge_Handler__c 15 | 16 | 17 | Required 18 | Name 19 | 20 | 21 | 22 | 23 | Edit 24 | Active__c 25 | 26 | 27 | Edit 28 | Use_for_Matching__c 29 | 30 | 31 | Edit 32 | Keep_Least_Recent_Value__c 33 | 34 | 35 | Edit 36 | Keep_Most_Recent_Value__c 37 | 38 | 39 | Edit 40 | Keep_Null_Value__c 41 | 42 | 43 | Edit 44 | Treat_False_as_Null__c 45 | 46 | 47 | 48 | 49 | 50 | false 51 | true 52 | true 53 | 54 | 55 | 56 | Readonly 57 | CreatedById 58 | 59 | 60 | 61 | 62 | Readonly 63 | LastModifiedById 64 | 65 | 66 | 67 | 68 | 69 | true 70 | false 71 | true 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Object_Merge_Handler__c 80 | Name 81 | Use_for_Matching__c 82 | Active__c 83 | Keep_Least_Recent_Value__c 84 | Keep_Most_Recent_Value__c 85 | Keep_Null_Value__c 86 | Treat_False_as_Null__c 87 | 88 | false 89 | false 90 | false 91 | false 92 | false 93 | 94 | 00h0H00000eNmYV 95 | 4 96 | 0 97 | Default 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/layouts/Object_Merge_Handler__c-Child Object Merge Handler Layout.layout: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeOwnerOne 4 | Share 5 | Submit 6 | 7 | false 8 | true 9 | true 10 | 11 | 12 | 13 | Required 14 | Parent_Handler__c 15 | 16 | 17 | Required 18 | Name 19 | 20 | 21 | Required 22 | Object_Lookup_Field_API_Name__c 23 | 24 | 25 | Edit 26 | Order_of_Execution__c 27 | 28 | 29 | 30 | 31 | Edit 32 | Active__c 33 | 34 | 35 | Required 36 | Standard_Action__c 37 | 38 | 39 | Edit 40 | Merge_Action__c 41 | 42 | 43 | Edit 44 | Clone_Reparented_Victim__c 45 | 46 | 47 | 48 | 49 | 50 | false 51 | true 52 | true 53 | 54 | 55 | 56 | Readonly 57 | CreatedById 58 | 59 | 60 | Readonly 61 | LastModifiedById 62 | 63 | 64 | 65 | 66 | Edit 67 | RecordTypeId 68 | 69 | 70 | 71 | 72 | 73 | true 74 | false 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Name 84 | RecordTypeId 85 | Parent_Handler__c 86 | Order_of_Execution__c 87 | Object_Lookup_Field_API_Name__c 88 | Standard_Action__c 89 | Merge_Action__c 90 | Clone_Reparented_Victim__c 91 | Active__c 92 | 93 | 94 | MassChangeOwner 95 | NAME 96 | Use_for_Matching__c 97 | Keep_Least_Recent_Value__c 98 | Keep_Most_Recent_Value__c 99 | Keep_Null_Value__c 100 | Treat_False_as_Null__c 101 | Active__c 102 | Object_Merge_Field__c.Object_Merge_Handler__c 103 | NAME 104 | Asc 105 | 106 | false 107 | false 108 | false 109 | false 110 | false 111 | 112 | 00h0H00000eNpNG 113 | 4 114 | 0 115 | Default 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/layouts/Object_Merge_Handler__c-Parent Object Merge Handler Layout.layout: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeOwnerOne 4 | Share 5 | Submit 6 | 7 | false 8 | true 9 | true 10 | 11 | 12 | 13 | Required 14 | Name 15 | 16 | 17 | 18 | 19 | Edit 20 | Active__c 21 | 22 | 23 | 24 | 25 | 26 | false 27 | true 28 | true 29 | 30 | 31 | 32 | Edit 33 | RecordTypeId 34 | 35 | 36 | 37 | 38 | Readonly 39 | CreatedById 40 | 41 | 42 | Readonly 43 | LastModifiedById 44 | 45 | 46 | 47 | 48 | 49 | true 50 | false 51 | true 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Name 60 | RecordTypeId 61 | Active__c 62 | 63 | 64 | MassChangeOwner 65 | NAME 66 | Use_for_Matching__c 67 | Keep_Least_Recent_Value__c 68 | Keep_Most_Recent_Value__c 69 | Keep_Null_Value__c 70 | Treat_False_as_Null__c 71 | Active__c 72 | Object_Merge_Field__c.Object_Merge_Handler__c 73 | NAME 74 | Asc 75 | 76 | 77 | MassChangeOwner 78 | NAME 79 | Object_Lookup_Field_API_Name__c 80 | Standard_Action__c 81 | Merge_Action__c 82 | Order_of_Execution__c 83 | Active__c 84 | Object_Merge_Handler__c.Parent_Handler__c 85 | NAME 86 | Asc 87 | 88 | false 89 | false 90 | false 91 | false 92 | false 93 | 94 | 00h0H00000eNmYQ 95 | 4 96 | 0 97 | Default 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/layouts/Object_Merge_Pair__c-Object Merge Pair Layout.layout: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeOwnerOne 4 | ChangeRecordType 5 | Share 6 | Submit 7 | 8 | false 9 | true 10 | true 11 | 12 | 13 | 14 | Readonly 15 | Name 16 | 17 | 18 | Required 19 | Master_ID__c 20 | 21 | 22 | Required 23 | Victim_ID__c 24 | 25 | 26 | 27 | 28 | Readonly 29 | Merge_Date__c 30 | 31 | 32 | Edit 33 | Status__c 34 | 35 | 36 | Readonly 37 | Error_Reason__c 38 | 39 | 40 | 41 | 42 | 43 | true 44 | true 45 | true 46 | 47 | 48 | 49 | Edit 50 | DML_Exception_IDs__c 51 | 52 | 53 | Edit 54 | DML_Exception_Field_Names__c 55 | 56 | 57 | Edit 58 | Apex_Exception_Type__c 59 | 60 | 61 | Edit 62 | Apex_Exception_Message__c 63 | 64 | 65 | 66 | 67 | Edit 68 | DML_Exception_Types__c 69 | 70 | 71 | Edit 72 | DML_Exception_Messages__c 73 | 74 | 75 | Edit 76 | Apex_Exception_Line_Number__c 77 | 78 | 79 | Edit 80 | Apex_Exception_Stack_Trace__c 81 | 82 | 83 | 84 | 85 | 86 | false 87 | true 88 | true 89 | 90 | 91 | 92 | Readonly 93 | CreatedById 94 | 95 | 96 | 97 | 98 | Readonly 99 | LastModifiedById 100 | 101 | 102 | 103 | 104 | 105 | true 106 | false 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | false 115 | false 116 | false 117 | false 118 | false 119 | 120 | 00h0H00000eNmYa 121 | 4 122 | 0 123 | Default 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/objects/Object_Merge_Field__c.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | Accept 9 | Large 10 | Default 11 | 12 | 13 | Accept 14 | Small 15 | Default 16 | 17 | 18 | CancelEdit 19 | Default 20 | 21 | 22 | CancelEdit 23 | Large 24 | Default 25 | 26 | 27 | CancelEdit 28 | Small 29 | Default 30 | 31 | 32 | Clone 33 | Default 34 | 35 | 36 | Clone 37 | Large 38 | Default 39 | 40 | 41 | Clone 42 | Small 43 | Default 44 | 45 | 46 | Delete 47 | Default 48 | 49 | 50 | Delete 51 | Large 52 | Default 53 | 54 | 55 | Delete 56 | Small 57 | Default 58 | 59 | 60 | Edit 61 | Default 62 | 63 | 64 | Edit 65 | Large 66 | Default 67 | 68 | 69 | Edit 70 | Small 71 | Default 72 | 73 | 74 | List 75 | Default 76 | 77 | 78 | List 79 | Large 80 | Default 81 | 82 | 83 | List 84 | Small 85 | Default 86 | 87 | 88 | New 89 | Default 90 | 91 | 92 | New 93 | Large 94 | Default 95 | 96 | 97 | New 98 | Small 99 | Default 100 | 101 | 102 | SaveEdit 103 | Default 104 | 105 | 106 | SaveEdit 107 | Large 108 | Default 109 | 110 | 111 | SaveEdit 112 | Small 113 | Default 114 | 115 | 116 | Tab 117 | Default 118 | 119 | 120 | Tab 121 | Large 122 | Default 123 | 124 | 125 | Tab 126 | Small 127 | Default 128 | 129 | 130 | View 131 | Default 132 | 133 | 134 | View 135 | Large 136 | Default 137 | 138 | 139 | View 140 | Small 141 | Default 142 | 143 | false 144 | SYSTEM 145 | Deployed 146 | Represents a field to be merged. 147 | false 148 | false 149 | false 150 | true 151 | false 152 | true 153 | false 154 | false 155 | false 156 | ControlledByParent 157 | 158 | Active__c 159 | true 160 | If checked, this field will be included in processing. 161 | false 162 | If checked, this field will be included in processing. 163 | 164 | true 165 | false 166 | Checkbox 167 | 168 | 169 | Keep_Least_Recent_Value__c 170 | false 171 | If checked, the least recent value of this field according to Field History Tracking will be kept. 172 | false 173 | If checked, the least recent value of this field according to Field History Tracking will be kept. 174 | 175 | false 176 | false 177 | Checkbox 178 | 179 | 180 | Keep_Most_Recent_Value__c 181 | false 182 | If checked, the most recent value of this field according to Field History Tracking will be kept. 183 | false 184 | If checked, the most recent value of this field according to Field History Tracking will be kept. 185 | 186 | false 187 | false 188 | Checkbox 189 | 190 | 191 | Keep_Null_Value__c 192 | false 193 | If checked, null field values will be kept if more recent than non-null values when the "Keep Most Recent Value" checkbox is checked or if on the master record if not checked. 194 | false 195 | If checked, null field values will be kept if more recent than non-null values when the "Keep Most Recent Value" checkbox is checked or if on the master record if not checked. 196 | 197 | false 198 | false 199 | Checkbox 200 | 201 | 202 | Object_Merge_Handler__c 203 | Handler for this field. 204 | false 205 | Handler for this field. 206 | 207 | Object_Merge_Handler__c 208 | Object Merge Fields 209 | Object_Merge_Fields 210 | 0 211 | true 212 | false 213 | false 214 | MasterDetail 215 | false 216 | 217 | 218 | Treat_False_as_Null__c 219 | false 220 | When checked, the value of unchecked checkbox fields will be treated as null during the merge process. Unchecked checkbox fields on master records will be overwritten by checked checkbox fields on victim records. 221 | false 222 | When checked, the value of unchecked checkbox fields will be treated as null during the merge process. Unchecked checkbox fields on master records will be overwritten by checked checkbox fields on victim records. 223 | 224 | false 225 | false 226 | Checkbox 227 | 228 | 229 | Use_for_Matching__c 230 | false 231 | If checked, this field will be used to match to duplicate related records on the master record. 232 | false 233 | If checked, this field will be used to match to duplicate related records on the master record. 234 | 235 | false 236 | false 237 | Checkbox 238 | 239 | 240 | 241 | 242 | false 243 | Text 244 | 245 | Object Merge Fields 246 | 247 | ControlledByParent 248 | 249 | Keep_Most_Least_Recent_Value 250 | true 251 | Ensures that both the Keep Least Recent Value and Keep Most Recent Value checkboxes are not checked. 252 | Keep_Least_Recent_Value__c && Keep_Most_Recent_Value__c 253 | Both the Keep Least Recent Value checkbox and Keep Most Recent Value checkbox cannot be checked at the same time. 254 | 255 | Public 256 | 257 | -------------------------------------------------------------------------------- /src/objects/Object_Merge_Handler__c.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | Accept 9 | Large 10 | Default 11 | 12 | 13 | Accept 14 | Small 15 | Default 16 | 17 | 18 | CancelEdit 19 | Default 20 | 21 | 22 | CancelEdit 23 | Large 24 | Default 25 | 26 | 27 | CancelEdit 28 | Small 29 | Default 30 | 31 | 32 | Clone 33 | Default 34 | 35 | 36 | Clone 37 | Large 38 | Default 39 | 40 | 41 | Clone 42 | Small 43 | Default 44 | 45 | 46 | Delete 47 | Default 48 | 49 | 50 | Delete 51 | Large 52 | Default 53 | 54 | 55 | Delete 56 | Small 57 | Default 58 | 59 | 60 | Edit 61 | Default 62 | 63 | 64 | Edit 65 | Large 66 | Default 67 | 68 | 69 | Edit 70 | Small 71 | Default 72 | 73 | 74 | List 75 | Default 76 | 77 | 78 | List 79 | Large 80 | Default 81 | 82 | 83 | List 84 | Small 85 | Default 86 | 87 | 88 | New 89 | Default 90 | 91 | 92 | New 93 | Large 94 | Default 95 | 96 | 97 | New 98 | Small 99 | Default 100 | 101 | 102 | SaveEdit 103 | Default 104 | 105 | 106 | SaveEdit 107 | Large 108 | Default 109 | 110 | 111 | SaveEdit 112 | Small 113 | Default 114 | 115 | 116 | Tab 117 | Default 118 | 119 | 120 | Tab 121 | Large 122 | Default 123 | 124 | 125 | Tab 126 | Small 127 | Default 128 | 129 | 130 | View 131 | Default 132 | 133 | 134 | View 135 | Large 136 | Default 137 | 138 | 139 | View 140 | Small 141 | Default 142 | 143 | false 144 | SYSTEM 145 | Deployed 146 | Represents an object to be merged. 147 | false 148 | false 149 | false 150 | true 151 | false 152 | true 153 | false 154 | false 155 | false 156 | ReadWrite 157 | 158 | Active__c 159 | true 160 | If checked, this merge handler will be used during the Object merge process. 161 | false 162 | If checked, this merge handler will be used during the Object merge process. 163 | 164 | true 165 | false 166 | Checkbox 167 | 168 | 169 | Clone_Reparented_Victim__c 170 | false 171 | If checked, a victim record that would normally be reparented would instead be cloned. This is useful for master-detail relationships that don't allow reparenting. 172 | false 173 | If checked, a victim record that would normally be reparented would instead be cloned. This is useful for master-detail relationships that don't allow reparenting. 174 | 175 | false 176 | false 177 | Checkbox 178 | 179 | 180 | Merge_Action__c 181 | How the merge will be performed. 182 | false 183 | How the merge will be performed. 184 | 185 | false 186 | false 187 | false 188 | Picklist 189 | 190 | true 191 | 192 | false 193 | 194 | Keep Oldest Created 195 | false 196 | 197 | 198 | 199 | Keep Newest Created 200 | false 201 | 202 | 203 | 204 | Keep Last Modified 205 | false 206 | 207 | 208 | 209 | Delete Duplicate 210 | false 211 | 212 | 213 | 214 | Keep Master 215 | false 216 | 217 | 218 | 219 | 220 | 221 | 222 | Object_Lookup_Field_API_Name__c 223 | API name of the field on the related object that looks up to Object. 224 | false 225 | API name of the field on the related object that looks up to Object. 226 | 227 | 40 228 | false 229 | false 230 | false 231 | Text 232 | false 233 | 234 | 235 | Order_of_Execution__c 236 | Order in which this handler is executed. 237 | false 238 | Order in which this handler is executed. 239 | 240 | 3 241 | false 242 | 0 243 | false 244 | false 245 | Number 246 | false 247 | 248 | 249 | Parent_Handler__c 250 | SetNull 251 | Handler for the parent object. 252 | false 253 | Handler for the parent object. 254 | 255 | Object_Merge_Handler__c 256 | Object Merge Handlers 257 | Object_Merge_Handlers 258 | false 259 | false 260 | false 261 | Lookup 262 | 263 | 264 | Standard_Action__c 265 | Action to be performed on the victim record when a duplicate is not found. 266 | false 267 | Action to be performed on the victim record when a duplicate is not found. 268 | 269 | false 270 | false 271 | false 272 | Picklist 273 | 274 | true 275 | 276 | false 277 | 278 | Move Victim 279 | false 280 | 281 | 282 | 283 | Clone Victim 284 | false 285 | 286 | 287 | 288 | Delete Victim 289 | false 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | Active_Merge_Handlers 298 | NAME 299 | RECORDTYPE 300 | Order_of_Execution__c 301 | Standard_Action__c 302 | LAST_UPDATE 303 | Everything 304 | 305 | Active__c 306 | equals 307 | 1 308 | 309 | 310 | 311 | 312 | All 313 | NAME 314 | RECORDTYPE 315 | Parent_Handler__c 316 | Object_Lookup_Field_API_Name__c 317 | Standard_Action__c 318 | Merge_Action__c 319 | Clone_Reparented_Victim__c 320 | Order_of_Execution__c 321 | Active__c 322 | Everything 323 | 324 | 325 | 326 | Child_Handlers 327 | Parent_Handler__c 328 | NAME 329 | Object_Lookup_Field_API_Name__c 330 | Standard_Action__c 331 | Merge_Action__c 332 | Clone_Reparented_Victim__c 333 | Order_of_Execution__c 334 | Active__c 335 | Everything 336 | 337 | RECORDTYPE 338 | equals 339 | Object_Merge_Handler__c.Child_Handler 340 | 341 | 342 | 343 | 344 | Parent_Handlers 345 | NAME 346 | Active__c 347 | Everything 348 | 349 | RECORDTYPE 350 | equals 351 | Object_Merge_Handler__c.Parent_Handler 352 | 353 | 354 | 355 | 356 | 357 | false 358 | Text 359 | 360 | Object Merge Handlers 361 | false 362 | 363 | Child_Handler 364 | true 365 | Handler for a child object. 366 | 367 | 368 | Merge_Action__c 369 | 370 | Delete Duplicate 371 | false 372 | 373 | 374 | Keep Last Modified 375 | false 376 | 377 | 378 | Keep Master 379 | false 380 | 381 | 382 | Keep Newest Created 383 | false 384 | 385 | 386 | Keep Oldest Created 387 | false 388 | 389 | 390 | 391 | Standard_Action__c 392 | 393 | Clone Victim 394 | false 395 | 396 | 397 | Delete Victim 398 | false 399 | 400 | 401 | Move Victim 402 | false 403 | 404 | 405 | 406 | 407 | Parent_Handler 408 | true 409 | Handler for a parent object. 410 | 411 | 412 | Merge_Action__c 413 | 414 | Delete Duplicate 415 | false 416 | 417 | 418 | Keep Last Modified 419 | false 420 | 421 | 422 | Keep Master 423 | false 424 | 425 | 426 | Keep Newest Created 427 | false 428 | 429 | 430 | Keep Oldest Created 431 | false 432 | 433 | 434 | 435 | Standard_Action__c 436 | 437 | Clone Victim 438 | false 439 | 440 | 441 | Delete Victim 442 | false 443 | 444 | 445 | Move Victim 446 | false 447 | 448 | 449 | 450 | 451 | ReadWrite 452 | 453 | Parent_Handler_Populated 454 | true 455 | Ensures the Parent Handler field is populated on active Child Object Merge Handlers. 456 | Active__c && RecordType.DeveloperName == "Child_Handler" && ISBLANK(Parent_Handler__c) 457 | Parent_Handler__c 458 | The Parent Handler field must be populated on active Child Object Merge Handlers. 459 | 460 | 461 | Standard_Action_Populated 462 | true 463 | Ensures the Standard Action field is populated on active Child Object Merge Handlers. 464 | Active__c && RecordType.DeveloperName == "Child_Handler" && ISBLANK(TEXT(Standard_Action__c)) 465 | Standard_Action__c 466 | The Standard Action field must be populated on active Child Object Merge Handlers. 467 | 468 | Public 469 | 470 | -------------------------------------------------------------------------------- /src/objects/Object_Merge_Pair__c.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | Accept 9 | Large 10 | Default 11 | 12 | 13 | Accept 14 | Small 15 | Default 16 | 17 | 18 | CancelEdit 19 | Default 20 | 21 | 22 | CancelEdit 23 | Large 24 | Default 25 | 26 | 27 | CancelEdit 28 | Small 29 | Default 30 | 31 | 32 | Clone 33 | Default 34 | 35 | 36 | Clone 37 | Large 38 | Default 39 | 40 | 41 | Clone 42 | Small 43 | Default 44 | 45 | 46 | Delete 47 | Default 48 | 49 | 50 | Delete 51 | Large 52 | Default 53 | 54 | 55 | Delete 56 | Small 57 | Default 58 | 59 | 60 | Edit 61 | Default 62 | 63 | 64 | Edit 65 | Large 66 | Default 67 | 68 | 69 | Edit 70 | Small 71 | Default 72 | 73 | 74 | List 75 | Default 76 | 77 | 78 | List 79 | Large 80 | Default 81 | 82 | 83 | List 84 | Small 85 | Default 86 | 87 | 88 | New 89 | Default 90 | 91 | 92 | New 93 | Large 94 | Default 95 | 96 | 97 | New 98 | Small 99 | Default 100 | 101 | 102 | SaveEdit 103 | Default 104 | 105 | 106 | SaveEdit 107 | Large 108 | Default 109 | 110 | 111 | SaveEdit 112 | Small 113 | Default 114 | 115 | 116 | Tab 117 | Default 118 | 119 | 120 | Tab 121 | Large 122 | Default 123 | 124 | 125 | Tab 126 | Small 127 | Default 128 | 129 | 130 | View 131 | Default 132 | 133 | 134 | View 135 | Large 136 | Default 137 | 138 | 139 | View 140 | Small 141 | Default 142 | 143 | false 144 | SYSTEM 145 | Deployed 146 | Pair of object records that are merged. 147 | false 148 | false 149 | false 150 | true 151 | false 152 | true 153 | false 154 | false 155 | false 156 | ReadWrite 157 | 158 | Apex_Exception_Line_Number__c 159 | The line number the Apex exception occurred on. 160 | false 161 | The line number the Apex exception occurred on. 162 | 163 | 5 164 | false 165 | 0 166 | false 167 | false 168 | Number 169 | false 170 | 171 | 172 | Apex_Exception_Message__c 173 | The message of an Apex Exception. 174 | false 175 | The message of an Apex Exception. 176 | 177 | 32768 178 | false 179 | false 180 | LongTextArea 181 | 3 182 | 183 | 184 | Apex_Exception_Stack_Trace__c 185 | The stack trace of an Apex Exception. 186 | false 187 | The stack trace of an Apex Exception. 188 | 189 | 32768 190 | false 191 | false 192 | LongTextArea 193 | 3 194 | 195 | 196 | Apex_Exception_Type__c 197 | The name of the Apex Exception type. 198 | false 199 | The name of the Apex Exception type. 200 | 201 | 255 202 | false 203 | false 204 | false 205 | Text 206 | false 207 | 208 | 209 | DML_Exception_Field_Names__c 210 | Names of the fields that caused the DML Exception for all failed rows. 211 | false 212 | Names of the fields that caused the DML Exception for all failed rows. 213 | 214 | 32768 215 | false 216 | false 217 | LongTextArea 218 | 3 219 | 220 | 221 | DML_Exception_IDs__c 222 | IDs of all rows that had a DML Exception. 223 | false 224 | IDs of all rows that had a DML Exception. 225 | 226 | 32768 227 | false 228 | false 229 | LongTextArea 230 | 3 231 | 232 | 233 | DML_Exception_Messages__c 234 | DML Exception Messages for all failed rows. 235 | false 236 | DML Exception Messages for all failed rows. 237 | 238 | 32768 239 | false 240 | false 241 | LongTextArea 242 | 3 243 | 244 | 245 | DML_Exception_Types__c 246 | Types of DML Exceptions for all failed rows. 247 | false 248 | Types of DML Exceptions for all failed rows. 249 | 250 | 32768 251 | false 252 | false 253 | LongTextArea 254 | 3 255 | 256 | 257 | Duplicate_Contact__c 258 | SetNull 259 | false 260 | 261 | Contact 262 | Object Merge Pairs 263 | Object_Merge_Pairs 264 | false 265 | false 266 | false 267 | Lookup 268 | 269 | 270 | Error_Reason__c 271 | Reason for an "Error" status. 272 | false 273 | Reason for an "Error" status. 274 | 275 | 255 276 | false 277 | false 278 | false 279 | Text 280 | false 281 | 282 | 283 | Master_ID__c 284 | ID of the record that is merged into. 285 | false 286 | ID of the record that is merged into. 287 | 288 | 18 289 | true 290 | false 291 | false 292 | Text 293 | false 294 | 295 | 296 | Merge_Date__c 297 | Date/time the merge was attempted/completed. 298 | false 299 | Date/time the merge was attempted/completed. 300 | 301 | false 302 | false 303 | false 304 | DateTime 305 | 306 | 307 | Status__c 308 | Status of the merge operation. 309 | false 310 | Status of the merge operation. 311 | 312 | false 313 | true 314 | false 315 | Picklist 316 | 317 | true 318 | 319 | false 320 | 321 | Merged 322 | false 323 | 324 | 325 | 326 | Error 327 | false 328 | 329 | 330 | 331 | Retry 332 | false 333 | 334 | 335 | 336 | Processing 337 | false 338 | 339 | 340 | 341 | 342 | 343 | 344 | Victim_ID__c 345 | ID of the record that is getting merged. 346 | false 347 | ID of the record that is getting merged. 348 | 349 | 18 350 | true 351 | false 352 | false 353 | Text 354 | false 355 | 356 | 357 | 358 | All 359 | NAME 360 | Master_ID__c 361 | Victim_ID__c 362 | Status__c 363 | Merge_Date__c 364 | Everything 365 | 366 | 367 | 368 | Errors 369 | NAME 370 | Master_ID__c 371 | Victim_ID__c 372 | Merge_Date__c 373 | Status__c 374 | Error_Reason__c 375 | Apex_Exception_Type__c 376 | Apex_Exception_Message__c 377 | DML_Exception_IDs__c 378 | DML_Exception_Types__c 379 | DML_Exception_Messages__c 380 | Everything 381 | 382 | Status__c 383 | equals 384 | Error 385 | 386 | 387 | 388 | 389 | Merged 390 | NAME 391 | Master_ID__c 392 | Victim_ID__c 393 | Merge_Date__c 394 | Everything 395 | 396 | Status__c 397 | equals 398 | Merged 399 | 400 | 401 | 402 | 403 | OMP-{0000000} 404 | 405 | false 406 | AutoNumber 407 | 408 | Object Merge Pairs 409 | 410 | ReadWrite 411 | Public 412 | 413 | -------------------------------------------------------------------------------- /src/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ObjectMergeDuplicateManagerController 5 | ObjectMergeDuplicateManagerTest 6 | ObjectMergeHandleUsers 7 | ObjectMergePairTriggerHandler 8 | ObjectMergePairTriggerHandlerTest 9 | ObjectMergeUtility 10 | ObjectMergeValidator 11 | ObjectMergeValidatorTest 12 | ApexClass 13 | 14 | 15 | ObjectMergeFieldTrigger 16 | ObjectMergeHandlerTrigger 17 | ObjectMergePairTrigger 18 | ApexTrigger 19 | 20 | 21 | ObjectMergeDuplicateManager 22 | AuraDefinitionBundle 23 | 24 | 25 | Object_Merge 26 | CustomApplication 27 | 28 | 29 | Object_Merge_Field__c.Active__c 30 | Object_Merge_Field__c.Keep_Least_Recent_Value__c 31 | Object_Merge_Field__c.Keep_Most_Recent_Value__c 32 | Object_Merge_Field__c.Keep_Null_Value__c 33 | Object_Merge_Field__c.Object_Merge_Handler__c 34 | Object_Merge_Field__c.Treat_False_as_Null__c 35 | Object_Merge_Field__c.Use_for_Matching__c 36 | Object_Merge_Handler__c.Active__c 37 | Object_Merge_Handler__c.Clone_Reparented_Victim__c 38 | Object_Merge_Handler__c.Merge_Action__c 39 | Object_Merge_Handler__c.Object_Lookup_Field_API_Name__c 40 | Object_Merge_Handler__c.Order_of_Execution__c 41 | Object_Merge_Handler__c.Parent_Handler__c 42 | Object_Merge_Handler__c.Standard_Action__c 43 | Object_Merge_Pair__c.Apex_Exception_Line_Number__c 44 | Object_Merge_Pair__c.Apex_Exception_Message__c 45 | Object_Merge_Pair__c.Apex_Exception_Stack_Trace__c 46 | Object_Merge_Pair__c.Apex_Exception_Type__c 47 | Object_Merge_Pair__c.DML_Exception_Field_Names__c 48 | Object_Merge_Pair__c.DML_Exception_IDs__c 49 | Object_Merge_Pair__c.DML_Exception_Messages__c 50 | Object_Merge_Pair__c.DML_Exception_Types__c 51 | Object_Merge_Pair__c.Duplicate_Contact__c 52 | Object_Merge_Pair__c.Error_Reason__c 53 | Object_Merge_Pair__c.Master_ID__c 54 | Object_Merge_Pair__c.Merge_Date__c 55 | Object_Merge_Pair__c.Status__c 56 | Object_Merge_Pair__c.Victim_ID__c 57 | CustomField 58 | 59 | 60 | Object_Merge_Field__c 61 | Object_Merge_Handler__c 62 | Object_Merge_Pair__c 63 | CustomObject 64 | 65 | 66 | Object_Merge_Handler__c 67 | Object_Merge_Pair__c 68 | CustomTab 69 | 70 | 71 | Object_Merge_Field__c-Object Merge Field Layout 72 | Object_Merge_Handler__c-Child Object Merge Handler Layout 73 | Object_Merge_Handler__c-Parent Object Merge Handler Layout 74 | Object_Merge_Pair__c-Object Merge Pair Layout 75 | Layout 76 | 77 | 78 | Object_Merge_Handler__c.Active_Merge_Handlers 79 | Object_Merge_Handler__c.All 80 | Object_Merge_Handler__c.Child_Handlers 81 | Object_Merge_Handler__c.Parent_Handlers 82 | Object_Merge_Pair__c.All 83 | Object_Merge_Pair__c.Errors 84 | Object_Merge_Pair__c.Merged 85 | ListView 86 | 87 | 88 | ObjectMerge_Duplicate_Manager 89 | PermissionSet 90 | 91 | 92 | Object_Merge_Handler__c.Child_Handler 93 | Object_Merge_Handler__c.Parent_Handler 94 | RecordType 95 | 96 | 97 | Object_Merge_Field__c.Keep_Most_Least_Recent_Value 98 | Object_Merge_Handler__c.Parent_Handler_Populated 99 | Object_Merge_Handler__c.Standard_Action_Populated 100 | ValidationRule 101 | 102 | 56.0 103 | 104 | -------------------------------------------------------------------------------- /src/permissionsets/ObjectMerge_Duplicate_Manager.permissionset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ObjectMergeDuplicateManagerController 5 | true 6 | 7 | Permissions needed to use the ObjectMerge Duplicate Manager Lightning Component. 8 | 9 | false 10 | Object_Merge_Pair__c.Error_Reason__c 11 | true 12 | 13 | 14 | false 15 | Object_Merge_Pair__c.Merge_Date__c 16 | true 17 | 18 | 19 | true 20 | Object_Merge_Pair__c.Status__c 21 | true 22 | 23 | false 24 | 25 | 26 | false 27 | false 28 | false 29 | true 30 | false 31 | DuplicateRecordSet 32 | false 33 | 34 | 35 | true 36 | false 37 | true 38 | true 39 | false 40 | Object_Merge_Pair__c 41 | false 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/tabs/Object_Merge_Handler__c.tab: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Custom19: Wrench 5 | 6 | -------------------------------------------------------------------------------- /src/tabs/Object_Merge_Pair__c.tab: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Pairs of Contacts that are merged. 5 | Custom37: Bridge 6 | 7 | -------------------------------------------------------------------------------- /src/triggers/ObjectMergePairTrigger.trigger: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | trigger ObjectMergePairTrigger on Object_Merge_Pair__c (before insert, before update, after insert, after update) { 33 | 34 | if (Trigger.isBefore) { 35 | 36 | ObjectMergePairTriggerHandler.mergeRecords(Trigger.new); 37 | 38 | } else { 39 | 40 | ObjectMergePairTriggerHandler.handleUsers(Trigger.new); 41 | } 42 | } -------------------------------------------------------------------------------- /src/triggers/ObjectMergePairTrigger.trigger-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/triggers/objectMergeFieldTrigger.trigger: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | trigger ObjectMergeFieldTrigger on Object_Merge_Field__c (before insert, before update) { 33 | 34 | ObjectMergeValidator.validateObjectMergeFields(Trigger.new); 35 | } -------------------------------------------------------------------------------- /src/triggers/objectMergeFieldTrigger.trigger-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 53.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/triggers/objectMergeHandlerTrigger.trigger: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Kyle Schmid, Tondro Consulting, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | trigger ObjectMergeHandlerTrigger on Object_Merge_Handler__c (before insert, before update) { 33 | 34 | ObjectMergeValidator.validateObjectMergeHandlers(Trigger.new); 35 | } -------------------------------------------------------------------------------- /src/triggers/objectMergeHandlerTrigger.trigger-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /undeploy/destructiveChanges.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ObjectMerge 4 | Open-source solution for merging Salesforce objects and their related objects. This solution allows you to specify merging rules for parent objects, their fields, their related objects, and their related objects fields. 5 | 6 | ObjectMergeDuplicateManagerController 7 | ObjectMergeDuplicateManagerTest 8 | ObjectMergeHandleUsers 9 | ObjectMergePairTriggerHandler 10 | ObjectMergePairTriggerHandlerTest 11 | ObjectMergeUtility 12 | ObjectMergeValidator 13 | ObjectMergeValidatorTest 14 | ApexClass 15 | 16 | 17 | ObjectMergeFieldTrigger 18 | ObjectMergeHandlerTrigger 19 | ObjectMergePairTrigger 20 | ApexTrigger 21 | 22 | 23 | ObjectMergeDuplicateManager 24 | AuraDefinitionBundle 25 | 26 | 27 | Object_Merge 28 | CustomApplication 29 | 30 | 31 | Object_Merge_Field__c.Active__c 32 | Object_Merge_Field__c.Keep_Least_Recent_Value__c 33 | Object_Merge_Field__c.Keep_Most_Recent_Value__c 34 | Object_Merge_Field__c.Keep_Null_Value__c 35 | Object_Merge_Field__c.Object_Merge_Handler__c 36 | Object_Merge_Field__c.Treat_False_as_Null__c 37 | Object_Merge_Field__c.Use_for_Matching__c 38 | Object_Merge_Handler__c.Active__c 39 | Object_Merge_Handler__c.Clone_Reparented_Victim__c 40 | Object_Merge_Handler__c.Merge_Action__c 41 | Object_Merge_Handler__c.Object_Lookup_Field_API_Name__c 42 | Object_Merge_Handler__c.Order_of_Execution__c 43 | Object_Merge_Handler__c.Parent_Handler__c 44 | Object_Merge_Handler__c.Standard_Action__c 45 | Object_Merge_Pair__c.Apex_Exception_Line_Number__c 46 | Object_Merge_Pair__c.Apex_Exception_Message__c 47 | Object_Merge_Pair__c.Apex_Exception_Stack_Trace__c 48 | Object_Merge_Pair__c.Apex_Exception_Type__c 49 | Object_Merge_Pair__c.DML_Exception_Field_Names__c 50 | Object_Merge_Pair__c.DML_Exception_IDs__c 51 | Object_Merge_Pair__c.DML_Exception_Messages__c 52 | Object_Merge_Pair__c.DML_Exception_Types__c 53 | Object_Merge_Pair__c.Error_Reason__c 54 | Object_Merge_Pair__c.Master_ID__c 55 | Object_Merge_Pair__c.Merge_Date__c 56 | Object_Merge_Pair__c.Status__c 57 | Object_Merge_Pair__c.Victim_ID__c 58 | CustomField 59 | 60 | 61 | Object_Merge_Field__c 62 | Object_Merge_Handler__c 63 | Object_Merge_Pair__c 64 | CustomObject 65 | 66 | 67 | Object_Merge_Handler__c 68 | Object_Merge_Pair__c 69 | CustomTab 70 | 71 | 72 | Object_Merge_Field__c-Object Merge Field Layout 73 | Object_Merge_Handler__c-Child Object Merge Handler Layout 74 | Object_Merge_Handler__c-Parent Object Merge Handler Layout 75 | Object_Merge_Pair__c-Object Merge Pair Layout 76 | Layout 77 | 78 | 79 | Object_Merge_Handler__c.Active_Merge_Handlers 80 | Object_Merge_Handler__c.All 81 | Object_Merge_Handler__c.Child_Handlers 82 | Object_Merge_Handler__c.Parent_Handlers 83 | Object_Merge_Pair__c.All 84 | Object_Merge_Pair__c.Errors 85 | Object_Merge_Pair__c.Merged 86 | ListView 87 | 88 | 89 | ObjectMerge_Duplicate_Manager 90 | PermissionSet 91 | 92 | 93 | Object_Merge_Handler__c.Child_Handler 94 | Object_Merge_Handler__c.Parent_Handler 95 | RecordType 96 | 97 | 98 | Object_Merge_Field__c.Keep_Most_Least_Recent_Value 99 | Object_Merge_Handler__c.Parent_Handler_Populated 100 | Object_Merge_Handler__c.Standard_Action_Populated 101 | ValidationRule 102 | 103 | 56.0 104 | 105 | -------------------------------------------------------------------------------- /undeploy/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ObjectMerge 4 | 56.0 5 | 6 | --------------------------------------------------------------------------------