Skip to content

Commit

Permalink
v1.6.25 - Adds support for distinct sums, averages, and more (#587)
Browse files Browse the repository at this point in the history
* checkpoint for work on #583 that allows most rollup operations to be flagged as distinct 
* Finishes work on #583 by adding support for Rollup__mdt.IsDistinct__c flag which, when true, de-duplicates children values prior to rolling them up to the parent
  • Loading branch information
jamessimone authored May 2, 2024
1 parent be66271 commit 908512e
Show file tree
Hide file tree
Showing 19 changed files with 411 additions and 303 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ As well, don't miss [the Wiki](../../wiki), which includes even more info for co

## Deployment & Setup

<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OawrAAC">
<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008Ob22AAC">
<img alt="Deploy to Salesforce"
src="./media/deploy-package-to-prod.png">
</a>

<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OawrAAC">
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008Ob22AAC">
<img alt="Deploy to Salesforce Sandbox"
src="./media/deploy-package-to-sandbox.png">
</a>
Expand Down Expand Up @@ -147,6 +147,7 @@ Within the `Rollup__mdt` custom metadata type, add a new record with fields:
- `Group By Fields (Comma-separated)` (optional) - filling this field out turns any existing rollup into a group by rollup using the API names of children-level fields as supplied. For example, you could turn a SUM-based rollup on Opportunity into a grouping rollup, supplying fields like `StageName, Name` to sum the rollup field on Opportunity and group the output by Stage and Name. Can be used in conjunction with the next fields, `Group By Row (Start/End) Delimiter`. For multi-currency orgs where you are rolling up currency fields affected by multi-currency, please read the advanced notes in the Multi-Currency section.
- `Group By Row Start Delimiter` (optional) - if set, this is the delimiter which prefaces each row in the group by rollup to delimit results. Defaults to `` if not supplied
- `Group By Row End Delimiter` (optional) - if set, this is the delimiter which is appended to each row in the group by rollup to delimit results. Defaults to a new line character if not supplied. Please also note that different new line characters are required depending on what type of field you're rolling up to - `<br>` will work for Rich Text fields, for example, while `\n` is the way to enter new lines for long text areas
- `Is Distinct` (optional, defaults to `false`) - de-duplicates the children item values for any parent prior to rolling them up
- `Is Full Record Set` (optional, defaults to `false`) - converts any rollup into a full recalculation (e.g. all _other_ child records associated with the parent(s) of the child records being passed in will be fetched and used in the rollup calculation)
- `Is Rollup Started From Parent` (optional, defaults to `false`) - if the the records being passed in are the parent records, check this field off. Apex Rollup will then go and retrieve the assorted children records before rolling the values up to the parents. If you are using `Is Rollup Started From Parent` and grandparent rollups with Tasks/Events (or anything with a polymorphic relationship field like `Who` or `What` on Task/Event; the `Parent` field on `Contact Point Address` is another example of such a field), you **must** also include a filter for `What.Type` or `Who.Type` in your `Child Object Where Clause` in order to proceed, e.g. `What.Type = 'Account'`.
- `Is Table Formatted` (optional, defaults to `false`) - set this to true _instead of_ using the `Group By Row Start Delimiter` and `Group By Row End Delimiter` if you are rolling up to a rich text field and you want the output to be a table.
Expand Down Expand Up @@ -222,6 +223,7 @@ These are the fields on the `Rollup Control` custom metadata type:
- `Should Run As` - a picklist dictating the preferred method for running rollup operations. Possible values are `Queueable`, `Batchable`, or `Synchronous Rollup`. By default, Apex Rollup runs asynchronously as a queueable. Only one queueable can be fired from a process that's already asynchronous, and while Apex Rollup automatically detects such things, if _another_ bit of code that runs _after_ Apex Rollup needs to use that Queueable, `Batchable` may be a better option. When set to `Synchronous Rollup`, all calculations occur prior to an insert / update / delete being finished on the children records.
- `Should Run Single Records Synchronously` - Apex Rollup typically uses the `Should Run As` picklist to determine the default execution context for rollups (which tends to be async). This checkbox deviates from that methodology by forcing single record updates to run sync (whenever possible), which helps with handling updates from datatables or other features using Lightning Data Service (LDS).
- `Should Skip Resetting Parent Fields` (defaults to false) - for full recalculations and REFRESH-based child item updates, Apex Rollup by default assumes that for a parent record with no matching children, the parent-level field should be reset. If this checkbox is set to true, those parent records without results will simply be ignored, and will not be updated.
- `Should Throw On Save Errors` (defaults to false) - by default, Apex Rollup does not throw when a parent-level update fails due to something like validation rules failing or errors being thrown from Flow/Apex. Set this to true if you'd like to bubble up save exceptions.
- `Trigger Or Invocable Name` - If you are using custom Apex, a schedulable, or rolling up by way of the Invocable action and can't use the Apex Rollup lookup field. Use the pattern `trigger_fieldOnCalcItem_to_rollupFieldOnTarget_rollup` - for example: 'trigger_opportunity_stagename_to_account_name_rollup' (use lowercase on the field names). If there is a matching Rollup Limit record, those rules will be used. The first part of the string comes from how a rollup has been invoked - either by `trigger`, `invocable`, or `schedule`. A scheduled flow still uses `invocable`!

</details>
Expand Down Expand Up @@ -271,6 +273,7 @@ Here are the arguments necessary to invoke Apex Rollup from a Flow / Process Bui
- `Group By Row Start Delimiter` (optional) - if set, this is the delimiter which prefaces each row in the group by rollup to delimit results. Defaults to `-` if not supplied
- `Group By Row End Delimiter` (optional) - if set, this is the delimiter which is appended to each row in the group by rollup to delimit results. Defaults to a new line character if not supplied. Please also note that different new line characters are required depending on what type of field you're rolling up to - `<br>` will work for Rich Text fields, for example, while `\n` is the way to enter new lines for long text areas
- `One To Many Grandparent Fields` (optional, Comma separated list) - see [Rollup Custom Metadata Field Breakdown](#rollup-custom-metadata-field-breakdown) for more info, used in conjunction with `Grandparent Relationship Field Path`
- `Is Distinct` (optional, defaults to `false`) - de-duplicates the children item values for any parent prior to rolling them up
- `Is Full Record Set` (optional) - converts any rollup into a full recalculation (e.g. all _other_ child records associated with the parent(s) of the child records being passed in will be fetched and used in the rollup calculation)
- `Is Rollup Started From Parent` (optional, defaults to `{!$GlobalConstant.False}`) - set to `{!$GlobalConstant.True}` if collection being passed in is the parent SObject, and you want to recalculate the defined rollup operation for the passed in parent records. Used in conjunction with `Child Object Type When Rollup Started From Parent`. If you are using `Is Rollup Started From Parent` and grandparent rollups with Tasks/Events (or anything with a polymorphic relationship field like `Who` or `What` on Task/Event; the `Parent` field on `Contact Point Address` is another example of such a field), you **must** also include a filter for `What.Type` or `Who.Type` in your `Child Object Where Clause` in order to proceed, e.g. `What.Type = 'Account'`.
- `Is Table Formatted` (optional, defaults to `false`) - set this to true _instead of_ using the `Group By Row Start Delimiter` and `Group By Row End Delimiter` if you are rolling up to a rich text field and you want the output to be a table.
Expand Down
105 changes: 61 additions & 44 deletions extra-tests/classes/RollupCalcItemSorterTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ private class RollupCalcItemSorterTests {
Date severalDaysAgo = System.today().addDays(-2);
Opportunity expectedFirstItem = new Opportunity(Amount = null, CloseDate = severalDaysAgo);
Opportunity expectedSecondItem = new Opportunity(Amount = 1, CloseDate = severalDaysAgo);
List<Opportunity> oppsToSort = new List<Opportunity>{
new Opportunity(Amount = 1, CloseDate = System.today()),
List<RollupCalculator.WinnowResult> oppsToSort = new List<RollupCalculator.WinnowResult>{
new RollupCalculator.WinnowResult(new Opportunity(Amount = 1, CloseDate = System.today())),
// this record should essentially be thrown out of sorting since it "loses" on the first ordering,
// which is on Amount
new Opportunity(Amount = 3, CloseDate = severalDaysAgo.addDays(-1)),
expectedSecondItem,
expectedFirstItem
new RollupCalculator.WinnowResult(new Opportunity(Amount = 3, CloseDate = severalDaysAgo.addDays(-1))),
new RollupCalculator.WinnowResult(expectedSecondItem),
new RollupCalculator.WinnowResult(expectedFirstItem)
};
oppsToSort.sort(sorter);

System.assertEquals(expectedFirstItem, oppsToSort[0]);
System.assertEquals(expectedSecondItem, oppsToSort[1]);
System.assertEquals(expectedFirstItem, oppsToSort[0].item);
System.assertEquals(expectedSecondItem, oppsToSort[1].item);
}

@IsTest
Expand All @@ -41,13 +41,18 @@ private class RollupCalcItemSorterTests {
Opportunity expectedThirdItem = new Opportunity(Amount = 2, CloseDate = today, Name = 'a');
Opportunity expectedSecondItem = new Opportunity(Amount = 1, CloseDate = today, Name = 'c');
Opportunity expectedFourthItem = new Opportunity(Amount = 2, CloseDate = today.addDays(1), Name = 'a');
List<Opportunity> oppsToSort = new List<Opportunity>{ expectedSecondItem, expectedFourthItem, expectedThirdItem, expectedFirstItem };
List<RollupCalculator.WinnowResult> oppsToSort = new List<RollupCalculator.WinnowResult>{
new RollupCalculator.WinnowResult(expectedSecondItem),
new RollupCalculator.WinnowResult(expectedFourthItem),
new RollupCalculator.WinnowResult(expectedThirdItem),
new RollupCalculator.WinnowResult(expectedFirstItem)
};
oppsToSort.sort(sorter);

System.assertEquals(expectedFirstItem, oppsToSort[0]);
System.assertEquals(expectedSecondItem, oppsToSort[1]);
System.assertEquals(expectedThirdItem, oppsToSort[2]);
System.assertEquals(expectedFourthItem, oppsToSort[3]);
System.assertEquals(expectedFirstItem, oppsToSort[0].item);
System.assertEquals(expectedSecondItem, oppsToSort[1].item);
System.assertEquals(expectedThirdItem, oppsToSort[2].item);
System.assertEquals(expectedFourthItem, oppsToSort[3].item);
}

@IsTest
Expand All @@ -62,15 +67,15 @@ private class RollupCalcItemSorterTests {
Date severalDaysAgo = System.today().addDays(-2);
Opportunity expectedFirstItem = new Opportunity(Amount = 1, CloseDate = System.today());
Opportunity expectedSecondItem = new Opportunity(Amount = 3, CloseDate = severalDaysAgo.addDays(-1));
List<Opportunity> oppsToSort = new List<Opportunity>{
new Opportunity(Amount = 3, CloseDate = severalDaysAgo.addDays(-1)),
expectedSecondItem,
expectedFirstItem
List<RollupCalculator.WinnowResult> oppsToSort = new List<RollupCalculator.WinnowResult>{
new RollupCalculator.WinnowResult(new Opportunity(Amount = 3, CloseDate = severalDaysAgo.addDays(-1))),
new RollupCalculator.WinnowResult(expectedSecondItem),
new RollupCalculator.WinnowResult(expectedFirstItem)
};
oppsToSort.sort(sorter);

System.assertEquals(expectedFirstItem, oppsToSort[0]);
System.assertEquals(expectedSecondItem, oppsToSort[1]);
System.assertEquals(expectedFirstItem, oppsToSort[0].item);
System.assertEquals(expectedSecondItem, oppsToSort[1].item);
}

@IsTest
Expand All @@ -88,29 +93,33 @@ private class RollupCalcItemSorterTests {
Opportunity expectedSecondItem = new Opportunity(Amount = 5, CloseDate = today);
Opportunity expectedThirdItem = new Opportunity(Amount = 1, CloseDate = today);

List<Opportunity> oppsToSort = new List<Opportunity>{ expectedThirdItem, expectedFirstItem, expectedSecondItem };
List<RollupCalculator.WinnowResult> oppsToSort = new List<RollupCalculator.WinnowResult>{
new RollupCalculator.WinnowResult(expectedThirdItem),
new RollupCalculator.WinnowResult(expectedFirstItem),
new RollupCalculator.WinnowResult(expectedSecondItem)
};
oppsToSort.sort(sorter);

System.assertEquals(expectedFirstItem, oppsToSort[0]);
System.assertEquals(expectedSecondItem, oppsToSort[1]);
System.assertEquals(expectedThirdItem, oppsToSort[2]);
System.assertEquals(expectedFirstItem, oppsToSort[0].item);
System.assertEquals(expectedSecondItem, oppsToSort[1].item);
System.assertEquals(expectedThirdItem, oppsToSort[2].item);
}

@IsTest
static void shouldProperlySortPicklists() {
RollupCalcItemSorter sorter = new RollupCalcItemSorter(new List<RollupOrderBy__mdt>{ new RollupOrderBy__mdt(Ranking__c = 0, FieldName__c = 'Industry') });

List<Schema.PicklistEntry> picklistEntries = Account.Industry.getDescribe().getPicklistValues();
List<Account> accs = new List<Account>();
List<RollupCalculator.WinnowResult> accs = new List<RollupCalculator.WinnowResult>();

for (Integer reverseIndex = picklistEntries.size() - 1; reverseIndex >= 0; reverseIndex--) {
Schema.PicklistEntry entry = picklistEntries[reverseIndex];
accs.add(new Account(Name = entry.getValue(), Industry = entry.getValue()));
accs.add(new RollupCalculator.WinnowResult(new Account(Name = entry.getValue(), Industry = entry.getValue())));
}
accs.sort(sorter);

for (Integer index = 0; index < accs.size(); index++) {
System.assertEquals(picklistEntries[index].getValue(), accs[index].Industry, 'Account at index: ' + index + ' should have matched');
System.assertEquals(picklistEntries[index].getValue(), accs[index].item.get(Account.Industry), 'Account at index: ' + index + ' should have matched');
}
}

Expand All @@ -129,11 +138,14 @@ private class RollupCalcItemSorterTests {
}
);

List<Opportunity> opps = new List<Opportunity>{ second, opp };
List<RollupCalculator.WinnowResult> opps = new List<RollupCalculator.WinnowResult>{
new RollupCalculator.WinnowResult(second),
new RollupCalculator.WinnowResult(opp)
};
opps.sort(sorter);

System.assertEquals(opp, opps[0]);
System.assertEquals(second, opps[1]);
System.assertEquals(opp, opps[0].item);
System.assertEquals(second, opps[1].item);
}

@IsTest
Expand All @@ -144,32 +156,37 @@ private class RollupCalcItemSorterTests {

Opportunity expectedFirstItem = new Opportunity(Amount = null);
Opportunity expectedSecondItem = new Opportunity(Amount = 1);
List<Opportunity> oppsToSort = new List<Opportunity>{ new Opportunity(Amount = 1), new Opportunity(Amount = 3), expectedSecondItem, expectedFirstItem };
List<RollupCalculator.WinnowResult> oppsToSort = new List<RollupCalculator.WinnowResult>{
new RollupCalculator.WinnowResult(new Opportunity(Amount = 1)),
new RollupCalculator.WinnowResult(new Opportunity(Amount = 3)),
new RollupCalculator.WinnowResult(expectedSecondItem),
new RollupCalculator.WinnowResult(expectedFirstItem)
};
oppsToSort.sort(sorter);

System.assert(true, 'Should make it here');
}

@IsTest
static void sortsFieldNames() {
List<Opportunity> itemsToSort = new List<Opportunity>{
new Opportunity(StageName = 'Two'),
new Opportunity(StageName = 'Uno Reverse Card'),
new Opportunity(StageName = 'Two'),
new Opportunity(),
new Opportunity(StageName = 'Z'),
new Opportunity(StageName = 'One'),
new Opportunity(StageName = 'One')
List<RollupCalculator.WinnowResult> itemsToSort = new List<RollupCalculator.WinnowResult>{
new RollupCalculator.WinnowResult(new Opportunity(StageName = 'Two')),
new RollupCalculator.WinnowResult(new Opportunity(StageName = 'Uno Reverse Card')),
new RollupCalculator.WinnowResult(new Opportunity(StageName = 'Two')),
new RollupCalculator.WinnowResult(new Opportunity()),
new RollupCalculator.WinnowResult(new Opportunity(StageName = 'Z')),
new RollupCalculator.WinnowResult(new Opportunity(StageName = 'One')),
new RollupCalculator.WinnowResult(new Opportunity(StageName = 'One'))
};

itemsToSort.sort(new RollupCalcItemSorter(new List<String>{ Opportunity.Name.getDescribe().getName(), Opportunity.StageName.getDescribe().getName() }));

System.assertEquals(null, itemsToSort.get(0).StageName);
System.assertEquals('One', itemsToSort.get(1).StageName);
System.assertEquals('One', itemsToSort.get(2).StageName);
System.assertEquals('Two', itemsToSort.get(3).StageName);
System.assertEquals('Two', itemsToSort.get(4).StageName);
System.assertEquals('Uno Reverse Card', itemsToSort.get(5).StageName);
System.assertEquals('Z', itemsToSort.get(6).StageName);
System.assertEquals(null, itemsToSort.get(0).item.get(Opportunity.StageName));
System.assertEquals('One', itemsToSort.get(1).item.get(Opportunity.StageName));
System.assertEquals('One', itemsToSort.get(2).item.get(Opportunity.StageName));
System.assertEquals('Two', itemsToSort.get(3).item.get(Opportunity.StageName));
System.assertEquals('Two', itemsToSort.get(4).item.get(Opportunity.StageName));
System.assertEquals('Uno Reverse Card', itemsToSort.get(5).item.get(Opportunity.StageName));
System.assertEquals('Z', itemsToSort.get(6).item.get(Opportunity.StageName));
}
}
Loading

0 comments on commit 908512e

Please sign in to comment.