├── .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 |
32 | false
33 |
34 |
35 | true
36 | false
37 | true
38 | true
39 | false
40 |
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 |
--------------------------------------------------------------------------------