Skip to content

Commit

Permalink
Merge pull request #1518 from SFDO-Community/feature/1038-case-merge-…
Browse files Browse the repository at this point in the history
…error-when-parent-object
  • Loading branch information
aheber authored Feb 10, 2025
2 parents 5e9c7dd + 9d67047 commit ab605ec
Show file tree
Hide file tree
Showing 10 changed files with 598 additions and 236 deletions.
451 changes: 240 additions & 211 deletions dlrs/main/classes/RollupService.cls

Large diffs are not rendered by default.

298 changes: 298 additions & 0 deletions dlrs/main/classes/RollupServiceMergeTest.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
@IsTest
private class RollupServiceMergeTest {
// standard merge logic - https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_merge_statements.htm

/*
used to validate that a standard merge path is functional
the expectation is that when DLRS sees a merge it will add a
scheduled item record so the surving record can get recalculated
*/
@IsTest
static void testMergeWithMergedObjectBothParentAndChild() {
mockContactRollupCache();
// create a few contacts, merge them together
Contact c1 = new Contact(LastName = 'Test1');
Contact c2 = new Contact(LastName = 'Test2');
insert new List<Contact>{ c1, c2 };
Test.startTest();
merge c1 c2;
Test.stopTest();

// make sure a scheduled item record was added as a result of the merge
List<LookupRollupSummaryScheduleItems__c> items = [
SELECT Id, ParentId__c, QualifiedParentID__c
FROM LookupRollupSummaryScheduleItems__c
];
Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
LookupRollupSummaryScheduleItems__c i = items[0];
Assert.areEqual(c1.Id + '#m0000000000000000E', i.QualifiedParentID__c);
}

/*
this doesn't depend on the triggers, it allows us to prove that we understand
the setup for the same scenario as above and helps to concicely test this one area of code
it is also validation for proving we can use this pattern for additional scenarios
*/
@IsTest
static void testDirectMergeWithDelete() {
mockContactRollupCache();

// simulate a record with a merged record id
Contact c1 = (Contact) JSON.deserialize(
JSON.serialize(
new Map<String, Object>{
'Id' => '00300000000000000B',
'MasterRecordId' => '00300000000000000A'
}
),
Schema.Contact.class
);

// simulate AFTER_DELETE trigger where record has a 'MasterRecordId' but Trigger.new is null
RollupService.handleRollups(
new Map<Id, SObject>{ c1.Id => c1 },
null,
Schema.Contact.getSObjectType(),
new List<RollupSummaries.CalculationMode>{
RollupSummaries.CalculationMode.Realtime
}
);

// make sure a scheduled item record was added as a result of the merge code
List<LookupRollupSummaryScheduleItems__c> items = [
SELECT Id, ParentId__c, QualifiedParentID__c
FROM LookupRollupSummaryScheduleItems__c
];
Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
LookupRollupSummaryScheduleItems__c i = items[0];
Assert.areEqual(
c1.MasterRecordId + '#m0000000000000000E',
i.QualifiedParentID__c
);
}

/*
simulate merge scenarios on cases
orgs can be configured for two different merge models on cases
delete merged case or keep it and set it to a specific status
because the test class can't change this setting for the org we will
simulate each of these scenarios
*/

/**
* if the case is setup to match other merge behavior, deleting the merged record
*/
@IsTest
static void testMergeCaseWithDelete() {
mockCaseRollupCache();

// simulate a record with a merged record id
Case c1 = (Case) JSON.deserialize(
JSON.serialize(
new Map<String, Object>{
'Id' => '50000000000000000B',
'MasterRecordId' => '50000000000000000A'
}
),
Schema.Case.class
);

// simulate AFTER_DELETE trigger where record has a 'MasterRecordId' but Trigger.new is null
RollupService.handleRollups(
new Map<Id, SObject>{ c1.Id => c1 },
null,
Schema.Case.getSObjectType(),
new List<RollupSummaries.CalculationMode>{
RollupSummaries.CalculationMode.Realtime
}
);

// make sure a scheduled item record was added as a result of the merge code
List<LookupRollupSummaryScheduleItems__c> items = [
SELECT Id, ParentId__c, QualifiedParentID__c
FROM LookupRollupSummaryScheduleItems__c
];
Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
LookupRollupSummaryScheduleItems__c i = items[0];
Assert.areEqual(
c1.MasterRecordId + '#m0000000000000000E',
i.QualifiedParentID__c
);
}

// case w/ keep
@IsTest
static void testMergeCaseWithKeep() {
mockCaseRollupCache();

// simulate a record with a merged record id
Case cOld = (Case) JSON.deserialize(
JSON.serialize(
new Map<String, Object>{
'Id' => '50000000000000000B',
'MasterRecordId' => null
}
),
Schema.Case.class
);
Case cNew = (Case) JSON.deserialize(
JSON.serialize(
new Map<String, Object>{
'Id' => '50000000000000000B',
'MasterRecordId' => '50000000000000000A'
}
),
Schema.Case.class
);

// simulate AFTER_UPDATE trigger where record has a 'MasterRecordId' but Trigger.new is null
RollupService.handleRollups(
new Map<Id, SObject>{ cOld.Id => cOld },
new Map<Id, SObject>{ cNew.Id => cNew },
Schema.Case.getSObjectType(),
new List<RollupSummaries.CalculationMode>{
RollupSummaries.CalculationMode.Realtime
}
);

// make sure a scheduled item record was added as a result of the merge code
List<LookupRollupSummaryScheduleItems__c> items = [
SELECT Id, ParentId__c, QualifiedParentID__c
FROM LookupRollupSummaryScheduleItems__c
];
Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
LookupRollupSummaryScheduleItems__c i = items[0];
Assert.areEqual(
cNew.MasterRecordId + '#m0000000000000000E',
i.QualifiedParentID__c
);
}

// case w/ edit on keep
@IsTest
static void testEditMergedCase() {
mockCaseRollupCache();

// simulate a record with a merged record id
Case cOld = (Case) JSON.deserialize(
JSON.serialize(
new Map<String, Object>{
'Id' => '50000000000000000B',
'MasterRecordId' => '50000000000000000A',
'Subject' => 'Subject 123'
}
),
Schema.Case.class
);
Case cNew = (Case) JSON.deserialize(
JSON.serialize(
new Map<String, Object>{
'Id' => '50000000000000000B',
'MasterRecordId' => '50000000000000000A',
'Subject' => 'Subject 456'
}
),
Schema.Case.class
);

// simulate AFTER_DELETE trigger where record has a 'MasterRecordId' but Trigger.new is null
RollupService.handleRollups(
new Map<Id, SObject>{ cOld.Id => cOld },
new Map<Id, SObject>{ cNew.Id => cNew },
Schema.Case.getSObjectType(),
new List<RollupSummaries.CalculationMode>{
RollupSummaries.CalculationMode.Realtime
}
);

// make sure a scheduled item record was added as a result of the merge code
List<LookupRollupSummaryScheduleItems__c> items = [
SELECT Id, ParentId__c, QualifiedParentID__c
FROM LookupRollupSummaryScheduleItems__c
];
Assert.areEqual(0, items.size(), 'Unexpected Rollup Items:' + items);
}

static void mockContactRollupCache() {
String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe()
.getKeyPrefix();
List<LookupRollupSummary2__mdt> rollups = new List<LookupRollupSummary2__mdt>{
new LookupRollupSummary2__mdt(
Id = prefix + '00000000000000D',
Label = 'Contact to Account',
DeveloperName = 'Contact_to_Account',
ParentObject__c = 'Account',
ChildObject__c = 'Contact',
RelationshipField__c = 'AccountId',
FieldToAggregate__c = 'Id',
AggregateOperation__c = 'Count',
AggregateResultField__c = 'NumberOfEmployees',
CalculationMode__c = 'Realtime',
AggregateAllRows__c = false,
Active__c = true
),
new LookupRollupSummary2__mdt(
Id = prefix + '00000000000000E',
Label = 'Asset to Contact',
DeveloperName = 'Asset_To_Contact',
ParentObject__c = 'Contact',
ChildObject__c = 'Asset',
RelationshipField__c = 'ContactId',
RelationshipCriteriaFields__c = 'ProductCode',
FieldToAggregate__c = 'SerialNumber',
AggregateOperation__c = 'First',
AggregateResultField__c = 'FirstName',
CalculationMode__c = 'Realtime',
AggregateAllRows__c = false,
Active__c = true
)
};
RollupSummariesSelector.setRollupCache(
false,
false,
RollupSummary.toList(rollups)
);
}

static void mockCaseRollupCache() {
String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe()
.getKeyPrefix();
List<LookupRollupSummary2__mdt> rollups = new List<LookupRollupSummary2__mdt>{
new LookupRollupSummary2__mdt(
Id = prefix + '00000000000000D',
Label = 'Case to Contact',
DeveloperName = 'Case_to_Contact',
ParentObject__c = 'Contact',
ChildObject__c = 'Case',
RelationshipField__c = 'ContactId',
RelationshipCriteriaFields__c = 'Subject',
FieldToAggregate__c = 'Id',
AggregateOperation__c = 'First',
AggregateResultField__c = 'FirstName',
CalculationMode__c = 'Realtime',
AggregateAllRows__c = false,
Active__c = true
),
new LookupRollupSummary2__mdt(
Id = prefix + '00000000000000E',
Label = 'Comment to Case',
DeveloperName = 'Comment_to_Case',
ParentObject__c = 'Case',
ChildObject__c = 'CaseComment',
RelationshipField__c = 'ParentId',
RelationshipCriteriaFields__c = 'CommentBody',
FieldToAggregate__c = 'CommentBody',
AggregateOperation__c = 'First',
AggregateResultField__c = 'Description',
CalculationMode__c = 'Realtime',
AggregateAllRows__c = false,
Active__c = true
)
};
RollupSummariesSelector.setRollupCache(
false,
false,
RollupSummary.toList(rollups)
);
}
}
5 changes: 5 additions & 0 deletions dlrs/main/classes/RollupServiceMergeTest.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<status>Active</status>
</ApexClass>
26 changes: 25 additions & 1 deletion dlrs/main/classes/RollupSummariesSelector.cls
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public class RollupSummariesSelector {
}

/**
* Returns active lookup rollup summary definitions for thr given calculation modes and child object
* Returns active lookup rollup summary definitions for the given calculation modes and child object
**/
public List<RollupSummary> selectActiveByChildObject(
List<RollupSummaries.CalculationMode> calculationModes,
Expand All @@ -155,6 +155,30 @@ public class RollupSummariesSelector {
return sortSummaries(records, 'ParentObject__c', 'RelationshipField__c');
}

/**
* Returns active lookup rollup summary definitions for the given calculation modes and child or parent object
**/
public List<RollupSummary> selectActiveByParentObject(
List<RollupSummaries.CalculationMode> calculationModes,
Set<String> objectNames
) {
List<String> rollupSummaryNames = new List<String>();
for (RollupSummaries.CalculationMode cm : calculationModes) {
rollupSummaryNames.add(cm.name());
}
List<RollupSummary> records = new List<RollupSummary>();
for (RollupSummary rs : this.allSummaries) {
if (
rs.Active &&
containsIgnoreCase(objectNames, rs.ParentObject) &&
containsIgnoreCase(rollupSummaryNames, rs.CalculationMode)
) {
records.add(rs);
}
}
return sortSummaries(records, 'ParentObject__c', 'RelationshipField__c');
}

/**
* Returns active lookup rollup summary definitions for the given rollup unique names
**/
Expand Down
9 changes: 5 additions & 4 deletions orgs/beta.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"orgName": "Declarative Lookup Rollup Summaries Tool - Beta Test Org",
"edition": "Developer",
"features": [
"EnableSetPasswordInApi"
],
"features": ["EnableSetPasswordInApi"],
"country": "US",
"language": "en_US",
"settings": {
Expand All @@ -21,6 +19,9 @@
"sessionSettings": {
"forceRelogin": false
}
},
"caseSettings": {
"caseMergeInLightning": true
}
}
}
}
9 changes: 5 additions & 4 deletions orgs/beta_prerelease.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"orgName": "Declarative Lookup Rollup Summaries Tool - Beta Test Org",
"edition": "Developer",
"features": [
"EnableSetPasswordInApi"
],
"features": ["EnableSetPasswordInApi"],
"country": "US",
"language": "en_US",
"release": "Preview",
Expand All @@ -22,6 +20,9 @@
"sessionSettings": {
"forceRelogin": false
}
},
"caseSettings": {
"caseMergeInLightning": true
}
}
}
}
Loading

0 comments on commit ab605ec

Please sign in to comment.