Adds rule-based filtering for cloned records

Implements a mechanism to exclude child records from being cloned based on configurable formulas.

Introduces a Custom Metadata object to define exclusion rules, allowing administrators to specify formulas for different SObjects.

Updates the cloning service to evaluate these formulas and filter out records that meet the exclusion criteria.
This commit is contained in:
cgary committed 2025-04-21 16:39:07 -04:00
1 parent 81b24f298b
commit f34a61e653
67 files changed
+112 -11

No files matched your search

@@ -0,0 +1,2 @@
xœ•ÎM
1 @a×=E. $ý› ˆwiÒTG:ðø‚k7îßO·u]xJ§ÑÍ@U'A�FVÓ,F)rõ­F­ÄRdÏRÝ«t{�PRN*…ªÏI*Kh„1ÔÙ7d6%öJ®ã¾uØ[}þ]␍F/úXž7¸\�¦˜|8g8cDtúöq¿J÷ŽFDé
@@ -0,0 +1,4 @@
xœ•ÎÁ
!€áÎ>ż@ ŽŽ
Ñ»ŒãXKl® =~йK÷ÿƒ_¶u]&xOs¨btI3Jò•Äybn¡&ÌͲ”–Ù:6/úœÐ­Hë5”Ø]ÊDEmeÌèÑ1§–¼¯I«ácÞ·{ooØ·cˆÂ,�åyƒË\
ÑStálƒµF¾cSÿ æWi>¯ÑCÓ
@@ -0,0 +1,2 @@
xœ•ÎA
1 @Q×=E. ¤m&mA¼Kš¦:È8R+x|Áµ÷ÿÁ×}ÛÖ Á/‡9Ì ‹Ï‚¹ùŠÑ—ÆJ�¬´Ð‰c‰•b"²{Ȱû„Ê5ÕØXsW$ž f.Ü’ZÓÜ¥5qòš×}À³·7<÷×Pƒ9Doëý§3øDKàÄK�#¢ÓïØ´?ˆûUº©ID
@@ -0,0 +1,4 @@
xś•ÎA
Â0@Q×9Ĺ\@H2“¦ń.“ÉŚ©•4Ź/¸văţ?ř˛­ë2 †t]Ě(Ô2U¬1Ńdł™I#VMy´2×,d-¸w}`m%cDÎč˝z±9N¨)zndą”ăľuŘ­˝aߎ.
Łł<–ç␍.W™R$*>ĂŮ“÷NľcC˙ îWé>~
DŰ
@@ -0,0 +1,2 @@
xœ•ÎA
Â0@Q×9Å\@H&3ɤwI§S-ÒVÒ<¾àÚ�ûÿàë¾®K |éÍ b¦¬X“Äb¹VC?&Ï%Î�¢”0fBq¯Úlë 9 ià4'"1bŸ-)F?&RŽšeWÏþØóô†c?›ôVõ¹lw¸␍21’`a¸zòÞéw¬ÛÄý*ÝlÜAÝ
@@ -0,0 +1,2 @@
xœ•ÎM
1 @a×=E. ¤iâ]Ú4ÕAÆ‘N<¾àÚ�û÷Á“m]— ÎÆÓªÀ=&ÌVPkL܈(‡Œ wßšRwÜ,b2¯2ô9»‹…‹PMÔ’cñY{.9‡V) ±·,Õ”cÞ·{ooØ·cˆÂEËó—+Ø¢£ˆäáŒÑÈwlêÄü*ÍUCK
@@ -0,0 +1 @@
x°+)JMU06d01000PхMллcх*][И╥╗Ё╜NИмcmУWsYУv/
@@ -1 +1 @@
aedb3c7323a730a30e0cf8263e520ad4fe2a9912 3f635a585a79811712905e7ed8bacd891fb2dbd1
+24 -6
View File
@@ -1,6 +1,6 @@
{ {
"fileVersion": 0, "fileVersion": 0,
"serverMaxRevisionCounter": 104, "serverMaxRevisionCounter": 125,
"sourceMembers": { "sourceMembers": {
"ReportFolder__prm_channel_reports_folder": { "ReportFolder__prm_channel_reports_folder": {
"memberType": "ReportFolder", "memberType": "ReportFolder",
@@ -196,20 +196,20 @@
}, },
"Profile__Admin": { "Profile__Admin": {
"memberType": "Profile", "memberType": "Profile",
"serverRevisionCounter": 97, "serverRevisionCounter": 124,
"lastRetrievedFromServer": null, "lastRetrievedFromServer": null,
"isNameObsolete": false "isNameObsolete": false
}, },
"ApexClass__OpportunityCloneService": { "ApexClass__OpportunityCloneService": {
"memberType": "ApexClass", "memberType": "ApexClass",
"serverRevisionCounter": 45, "serverRevisionCounter": 115,
"lastRetrievedFromServer": 45, "lastRetrievedFromServer": 115,
"isNameObsolete": false "isNameObsolete": false
}, },
"ApexClass__OpportunityCloneServiceTest": { "ApexClass__OpportunityCloneServiceTest": {
"memberType": "ApexClass", "memberType": "ApexClass",
"serverRevisionCounter": 46, "serverRevisionCounter": 125,
"lastRetrievedFromServer": 46, "lastRetrievedFromServer": 125,
"isNameObsolete": false "isNameObsolete": false
}, },
"ApexClass__TestCloneUtility": { "ApexClass__TestCloneUtility": {
@@ -385,6 +385,24 @@
"serverRevisionCounter": 103, "serverRevisionCounter": 103,
"lastRetrievedFromServer": 103, "lastRetrievedFromServer": 103,
"isNameObsolete": false "isNameObsolete": false
},
"CustomMetadata__SmartCloneRecordExclusionRule.LineItemPriceBelowFiftyDollars": {
"memberType": "CustomMetadata",
"serverRevisionCounter": 112,
"lastRetrievedFromServer": null,
"isNameObsolete": true
},
"CustomMetadata__SmartCloneRecordExclusionRule.QuantityLessThanTwo": {
"memberType": "CustomMetadata",
"serverRevisionCounter": 116,
"lastRetrievedFromServer": 116,
"isNameObsolete": false
},
"StaticResource__SmartClone_TestData": {
"memberType": "StaticResource",
"serverRevisionCounter": 117,
"lastRetrievedFromServer": 117,
"isNameObsolete": false
} }
} }
} }
@@ -19,6 +19,8 @@ public with sharing class OpportunityCloneService {
* @description Map of Child relationship objects to the opportunity. * @description Map of Child relationship objects to the opportunity.
*/ */
public static Map<String,Schema.DescribeSObjectResult> opportunityChildrenMap; public static Map<String,Schema.DescribeSObjectResult> opportunityChildrenMap;
public static Map<String,SmartCloneRecordExclusionRule__mdt> exclusionRuleMap;
public static Map<String,List<FormulaEval.FormulaInstance>> formulaInstanceBySObjectMap;
/** /**
* @description static constructor that will populate the opportunityChildrenMap * @description static constructor that will populate the opportunityChildrenMap
*/ */
@@ -43,6 +45,24 @@ public with sharing class OpportunityCloneService {
} }
} }
} }
exclusionRuleMap = new Map<String,SmartCloneRecordExclusionRule__mdt>();
formulaInstanceBySObjectMap = new Map<String,List<FormulaEval.FormulaInstance>>();
Map<String,Schema.SObjectType> sObjectTypeMap = Schema.getGlobalDescribe();
for(SmartCloneRecordExclusionRule__mdt rule:SmartCloneRecordExclusionRule__mdt.getAll().values()){
if(rule.IsActive__c && String.isNotBlank(rule.ExclusionFormula__c)){
exclusionRuleMap.put(rule.DeveloperName,rule);
String sObjectName = rule.RelatedObject__c;
FormulaEval.FormulaInstance fInstance = Formula.builder().withType(sObjectTypeMap.get(sObjectName.toLowerCase()))
.withReturnType(FormulaEval.FormulaReturnType.Boolean)
.withFormula(rule.ExclusionFormula__c).build();
List<FormulaEval.FormulaInstance> fInstanceList = formulaInstanceBySObjectMap.get(sObjectName);
if(fInstanceList == null){
fInstanceList = new List<FormulaEval.FormulaInstance>();
}
fInstanceList.add(fInstance);
formulaInstanceBySObjectMap.put(sObjectName,fInstanceList);
}
}
} }
/** /**
* @description Lightning Exposed method that will provide a map of Child relationships * @description Lightning Exposed method that will provide a map of Child relationships
@@ -185,17 +205,31 @@ public with sharing class OpportunityCloneService {
} }
private static List<SObject> CloneAndReassociateRecords(List<SObject> recList, String parentFieldName, Id newParentId) { private static List<SObject> CloneAndReassociateRecords(List<SObject> recList, String parentFieldName, Id newParentId) {
System.debug(LoggingLevel.INFO,'\n\nOpportunityCloneService.CloneAndReassociateRecords Executing.\n\n ');
List<SObject> newList = new List<sObject>(); List<SObject> newList = new List<sObject>();
for(SObject rec:recList){ for(SObject rec:recList){
SObject newRec = rec.clone(false,true,false,false); SObject newRec = rec.clone(false,true,false,false);
newRec.put(parentFieldName, newParentId); newRec.put(parentFieldName, newParentId);
/** String sObjectName = newRec.getSObjectType().getDescribe().getName();
if(newRec.getSObjectType().getDescribe().getName().toUpperCase() == 'OPPORTUNITYLINEITEM') { if(sObjectName.toUpperCase() == 'OPPORTUNITYLINEITEM') {
newRec.put('UnitPrice',null); newRec.put('UnitPrice',null);
} }
*/ //TODO: This is where we can evaluate the record to determine if it meets the formula criteria being included
// as a child record.
Boolean fResult = false;
if(formulaInstanceBySObjectMap.containsKey(sObjectName)){
List<FormulaEval.FormulaInstance> fInstanceList = formulaInstanceBySObjectMap.get(sObjectName);
for(FormulaEval.FormulaInstance fInstance:fInstanceList){
fResult = (Boolean)fInstance.evaluate(newRec);
if(fResult) {
break;
}
}
}
if(!fResult) {
newList.add(newRec); newList.add(newRec);
} }
}
return newList; return newList;
} }
@@ -70,6 +70,7 @@ private class OpportunityCloneServiceTest {
} }
} }
insert oliTestLines; insert oliTestLines;
testFutureMethod();
} }
@isTest @isTest
@@ -106,10 +107,11 @@ private class OpportunityCloneServiceTest {
} }
@isTest @isTest
static void CloneOpportunityWithChildren(){ static void CloneOpportunityWithChildren(){
Test.startTest();
Opportunity testOpp = [SELECT Id, Name FROM Opportunity LIMIT 1]; Opportunity testOpp = [SELECT Id, Name FROM Opportunity LIMIT 1];
Map<String,String> testReplacements = new Map<String,String>(); Map<String,String> testReplacements = new Map<String,String>();
testReplacements.put('Name',TestCloneUtility.GenerateRandomString(10)); testReplacements.put('Name',TestCloneUtility.GenerateRandomString(10));
Test.startTest();
Id testResult = OpportunityCloneService.CloneOpportunityWithChildren(testOpp.Id, testReplacements, Id testResult = OpportunityCloneService.CloneOpportunityWithChildren(testOpp.Id, testReplacements,
new List<String>{'OpportunityLineItems'}); new List<String>{'OpportunityLineItems'});
@@ -117,4 +119,9 @@ private class OpportunityCloneServiceTest {
Test.stopTest(); Test.stopTest();
} }
@future
public static void testFutureMethod(){
//insert some Exlusion rules
Test.loadData(SmartCloneRecordExclusionRule__mdt.SObjectType, 'SmartClone_TestData');
}
} }
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>QuantityLessThanTwo</label>
<protected>false</protected>
<values>
<field>ExclusionFormula__c</field>
<value xsi:type="xsd:string">Quantity &lt; 2</value>
</values>
<values>
<field>IsActive__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>RelatedObject__c</field>
<value xsi:type="xsd:string">OpportunityLineItem</value>
</values>
</CustomMetadata>
@@ -0,0 +1,2 @@
DeveloperName,MasterLabel,IsActive__c,RelatedObject__c,ExclusionFormula__c
QuantityLessThanThree,QuantityLessThanThree,TRUE,OpportunityLineItem,Quantity < 3
1 DeveloperName MasterLabel IsActive__c RelatedObject__c ExclusionFormula__c
2 QuantityLessThanThree QuantityLessThanThree TRUE OpportunityLineItem Quantity < 3
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
<cacheControl>Public</cacheControl>
<contentType>text/csv</contentType>
<description>Test Data for SmartClone Code Coverage.</description>
</StaticResource>