Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.6.6 - Continued Flow Improvements #543

Merged
merged 9 commits into from
Dec 8, 2023
5 changes: 3 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=04t6g000008OaAXAA0">
<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaFiAAK">
<img alt="Deploy to Salesforce"
src="./media/deploy-package-to-prod.png">
</a>

<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaAXAA0">
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaFiAAK">
<img alt="Deploy to Salesforce Sandbox"
src="./media/deploy-package-to-sandbox.png">
</a>
Expand Down Expand Up @@ -283,6 +283,7 @@ Here are the arguments necessary to invoke Apex Rollup from a Flow / Process Bui
- `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.
- `Limit Amount` (optional) - can be used in conjunction with `Order By (First/Last)` below, otherwise the `Id` for each child object will be used as the implicit ordering. Allows rollup operations to enforce a limit on the number of matching calc items to use, to enable rollups like: "sum of the top five opportunities by amount".
- `Order By (First/Last)` (optional) - only valid when FIRST/LAST/MOST is used as the Rollup Operation (alternatively, if a `Limit Amount` is stipulated). Accepts a comma-separated list associated with the fields you'd like to order by, including the sort order and null sort order. For example: `Name nulls last, Industry` would use the Name field on a record with nulls last, followed by a sort on the Industry field to do tie-breakers. This field is optional on a first/last/most operation, and if a field is not supplied, the `Rollup Field On Child Object` is used.
- `Parent Record Id for Empty Children Collections` (optional) - Runs rollup calculations even if no matching children when passed. Must also fill out `Child Object Type When Rollup Started From Parent` property as well.
- `Sharing Mode` (optional) - defaults to `System` (without sharing) but can be set to `User` to run with sharing criteria.
- `Should rollup to ultimate hierarchy parent` (optional) - Check this box if you are rolling up to an Account, for example, and use the `Parent Account ID` field on accounts, _and_ want the rolled up value to only be used on the top-level account. Can be used with any hierarchy lookup or lookup back to the same object. Must be used in conjunction with `Ultimate Parent Lookup` (below), and _can_ be used in conjunction with `Grandparent Relationship Field Path` (if the hierarchical field you are rolling up to is not on the immediate parent object).
- `Should run sync` (optional) - Check this box if you'd like the parent items to be updated in a synchronous context. Note that if `Defer Processing` is set to true, the synchronous bit will only come into play whenever the `Process Deferred Rollup` action is called.
Expand Down
18 changes: 18 additions & 0 deletions extra-tests/classes/RollupFlowTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1014,4 +1014,22 @@ private class RollupFlowTests {
Account updatedAcc = [SELECT Id, AnnualRevenue FROM Account WHERE Id = :acc.Id];
System.assertEquals(expectedMost.PreferenceRank, updatedAcc.AnnualRevenue);
}

@IsTest
static void runsWithoutChildrenWhenFlagged() {
Account acc = [SELECT Id, AnnualRevenue FROM Account];
acc.AnnualRevenue = 50;
RollupAsyncProcessor.stubParentRecords = new List<SObject>{ acc };

List<Rollup.FlowInput> flowInputs = RollupTestUtils.prepareFlowTest(new List<SObject>(), 'REFRESH', 'SUM');
flowInputs[0].parentRecordIdForEmptyChildrenCollections = acc.Id;
flowInputs[0].calcItemTypeWhenRollupStartedFromParent = 'ContactPointAddress';

Test.startTest();
List<Rollup.FlowOutput> flowOutputs = Rollup.performRollup(flowInputs);
Test.stopTest();

System.assertNotEquals('No records to rollup, returning early', flowOutputs[0].message);
System.assertEquals(null, acc.AnnualRevenue);
}
}
13 changes: 12 additions & 1 deletion extra-tests/classes/RollupMultiCurrencyTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ private class RollupMultiCurrencyTests {
upsert new RollupSettings__c(IsEnabled__c = true);
}

@IsTest
static void doesNotQueryDatedCurrenciesForNonMatchingObject() {
Account acc = new Account();

RollupCurrencyInfo.transform(new List<Account>{ acc }, Account.AnnualRevenue, getCurrencyIsoCode(acc), new List<RollupOrderBy__mdt>());

// this test was flapping when comparing query counts during packaging despite passing in each multi-currency org
// previously. normally I don't like to resort to exposing @TestVisible properties but that's preferable to a flapping test
Assert.isFalse(RollupCurrencyInfo.hasLoadedDatedCurrencyInfo);
}

@IsTest
static void shouldCorrectlyRollupMaxForMultiCurrency() {
Account acc = (Account) RollupTestUtils.queryRecord(Account.SObjectType, new List<Schema.SObjectField>{ Account.AnnualRevenue });
Expand Down Expand Up @@ -347,7 +358,7 @@ private class RollupMultiCurrencyTests {
System.assertEquals(lastOpportunityId, acc.Name, 'Should have taken last based on multi-currency Amount! Records: ' + opportunities);
}

private static String getCurrencyIsoCode(SObject record) {
private static String getCurrencyIsoCode(SObject record) {
return UserInfo.isMultiCurrencyOrganization() ? (String) record.get(RollupCurrencyInfo.CURRENCY_ISO_CODE_FIELD_NAME) : 'USD';
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apex-rollup",
"version": "1.6.5",
"version": "1.6.6",
"description": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
"repository": {
"type": "git",
Expand Down
4 changes: 2 additions & 2 deletions rollup-namespaced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ For more info, see the base `README`.

## Deployment & Setup

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

<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaAcAAK">
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaFnAAK">
<img alt="Deploy to Salesforce Sandbox"
src="./media/deploy-package-to-sandbox.png">
</a>
7 changes: 4 additions & 3 deletions rollup-namespaced/sfdx-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"default": true,
"package": "apex-rollup-namespaced",
"path": "rollup-namespaced/source/rollup",
"versionName": "Flow optimizations",
"versionNumber": "1.1.5.0",
"versionName": "Flow optimizations continued",
"versionNumber": "1.1.6.0",
"versionDescription": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
"releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest",
"unpackagedMetadata": {
Expand All @@ -29,6 +29,7 @@
"apex-rollup-namespaced@1.1.2-0": "04t6g000008Oa7YAAS",
"apex-rollup-namespaced@1.1.3": "04t6g000008Oa8lAAC",
"apex-rollup-namespaced@1.1.4": "04t6g000008Oa9ZAAS",
"apex-rollup-namespaced@1.1.5": "04t6g000008OaAcAAK"
"apex-rollup-namespaced@1.1.5": "04t6g000008OaAcAAK",
"apex-rollup-namespaced@1.1.6": "04t6g000008OaFnAAK"
}
}
63 changes: 43 additions & 20 deletions rollup/core/classes/Rollup.cls
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje
protected Rollup__mdt metadata;
protected Boolean isFullRecalc = false;
protected Boolean isNoOp;
protected Boolean isNoOpOverridden = false;
protected Boolean isCDCUpdate = false;
protected Integer queryCount;
protected RollupCalcItemReplacer calcItemReplacer;
Expand Down Expand Up @@ -704,7 +705,11 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje
global transient List<SObject> recordsToRollup = new List<SObject>();
@InvocableVariable(label='Prior records to rollup' description='The old version of the records for update/upsert')
global transient List<SObject> oldRecordsToRollup = new List<SObject>();

@InvocableVariable(
label='Parent Record Id for Empty Children Collections'
description='Runs rollup calculations even if no matching children when passed. Must also fill out Child Object Type When Rollup Started From Parent property'
)
global Id parentRecordIdForEmptyChildrenCollections;
@InvocableVariable(
label='Should rollup to ultimate hierarchy parent'
description='Used in conjunction with Ultimate Parent Field to drive hierarchical parent rollups'
Expand Down Expand Up @@ -739,11 +744,17 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje
private final String rollupContext;
private final SObjectType sObjectType;
private final Set<Rollup__mdt> rollupMetadata = new Set<Rollup__mdt>();
private final Map<Id, SObject> children = new Map<Id, SObject>();
private final Map<Id, SObject> oldChildren = new Map<Id, SObject>();
private final Boolean shouldOverrideNoOp;

private FlowInputWrapper(FlowInput flowInput, String rollupContext, SObjectType sObjectType) {
this.flowInput = flowInput;
this.rollupContext = rollupContext;
this.sObjectType = sObjectType;
this.shouldOverrideNoOp =
this.flowInput.parentRecordIdForEmptyChildrenCollections != null &&
this.flowInput.calcItemTypeWhenRollupStartedFromParent != null;
}
}

Expand All @@ -770,6 +781,17 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje
flowInput.rollupOperation = flowInput.rollupOperation.toUpperCase();
RollupLogger.Instance.log('processing invocable data:', flowInput, LoggingLevel.INFO);

if (
flowInput.parentRecordIdForEmptyChildrenCollections != null &&
flowInput.calcItemTypeWhenRollupStartedFromParent != null &&
flowInput.recordsToRollup?.isEmpty() == true
) {
Schema.DescribeSObjectResult childDescribe = RollupFieldInitializer.Current.getDescribeFromName(flowInput.calcItemTypeWhenRollupStartedFromParent);
SObject stubChild = childDescribe.getSObjectType().newSObject(childDescribe.getKeyPrefix() + '0'.repeat(12));
stubChild.put(flowInput.lookupFieldOnCalcItem, flowInput.parentRecordIdForEmptyChildrenCollections);
flowInput.recordsToRollup.add(stubChild);
}

if (flowInput.recordsToRollup?.isEmpty() != false) {
flowOutput.message = 'No records to rollup, returning early';
} else {
Expand Down Expand Up @@ -801,12 +823,9 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje
wrapper = new FlowInputWrapper(flowInput, rollupContext, sObjectType);
metaToInputWrapper.put(meta, wrapper);
}
if (wrapper.rollupMetadata.contains(meta)) {
wrapper.flowInput.recordsToRollup.addAll(flowInput.recordsToRollup);
wrapper.flowInput.oldRecordsToRollup.addAll(flowInput.oldRecordsToRollup);
} else {
wrapper.rollupMetadata.add(meta);
}
wrapper.children.putAll(flowInput.recordsToRollup);
wrapper.oldChildren.putAll(flowInput.oldRecordsToRollup);
wrapper.rollupMetadata.add(meta);
}
}
if (metaToInputWrapper.isEmpty()) {
Expand All @@ -822,25 +841,29 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje
// here we clone the list because it can get winnowed in "processCustomMetadata"
List<Rollup__mdt> metas = new List<Rollup__mdt>(wrapper.rollupMetadata);
Map<Schema.SObjectType, List<Rollup>> typeToWinnowedRollups;
wrapper.flowInput.recordsToRollup = replaceFlowInputCalcItemsIfNecessary(hashCodeToCalcItems, wrapper.flowInput.recordsToRollup, metas);
List<SObject> children = wrapper.children.values();
wrapper.flowInput.recordsToRollup = replaceFlowInputCalcItemsIfNecessary(hashCodeToCalcItems, children, metas);
Map<Id, SObject> oldFlowRecords = RollupFieldInitializer.Current.createSafeMap(
replaceFlowInputCalcItemsIfNecessary(hashCodeToCalcItems, wrapper.flowInput.oldRecordsToRollup, metas)
replaceFlowInputCalcItemsIfNecessary(hashCodeToCalcItems, wrapper.oldChildren.values(), metas)
);

if (wrapper.flowInput.deferProcessing) {
typeToWinnowedRollups = new Map<Schema.SObjectType, List<Rollup>>{ wrapper.sObjectType => new List<Rollup>() };
}

processCustomMetadata(
typeToWinnowedRollups != null ? typeToWinnowedRollups : localRollups,
metas,
wrapper.flowInput.recordsToRollup,
oldFlowRecords,
new Set<String>(),
wrapper.rollupContext,
fromInvocable,
typeToWrappedMeta
);
Map<Schema.SObjectType, List<Rollup>> typeToLocalRollups = typeToWinnowedRollups != null ? typeToWinnowedRollups : localRollups;
processCustomMetadata(typeToLocalRollups, metas, children, oldFlowRecords, new Set<String>(), wrapper.rollupContext, fromInvocable, typeToWrappedMeta);

if (wrapper.shouldOverrideNoOp) {
List<Rollup> rollups = typeToLocalRollups.get(wrapper.sObjectType);
if (rollups != null) {
for (Rollup roll : rollups) {
roll.isNoOp = false;
roll.isNoOpOverridden = true;
roll.calcItems = children;
}
}
}

if (typeToWinnowedRollups != null) {
for (Schema.SObjectType typeKey : typeToWinnowedRollups.keyset()) {
Expand All @@ -849,7 +872,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje
}

if (metas.isEmpty() == false) {
Rollup rollupConductor = getRollup(metas, wrapper.sObjectType, wrapper.flowInput.recordsToRollup, oldFlowRecords, null, fromInvocable);
Rollup rollupConductor = getRollup(metas, wrapper.sObjectType, children, oldFlowRecords, null, fromInvocable);
String logMessage = 'adding invocable rollup to list:';
if (wrapper.flowInput.deferProcessing) {
logMessage = 'deferring processing for rollup:';
Expand Down
1 change: 1 addition & 0 deletions rollup/core/classes/RollupCalculator.cls
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ public without sharing abstract class RollupCalculator {
for (Integer index = 0; index < fieldMappings.size(); index++) {
fieldMappings[index] = fieldMappings[index].trim();
}
RollupCurrencyInfo.overrideDatedMultiCurrency(this.metadata.CalcItem__c, fieldMappings);
}
}

Expand Down
Loading