Files
smartclone/force-app/main/default/classes/OpportunityCloneService.cls
T
cgary f34a61e653 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.
2025-04-21 16:39:07 -04:00

278 lines
14 KiB
OpenEdge ABL

/**
* @description This service exposes methods that contain logic to make cloning an opportunity
* much easier and more functional.
* @author Cloud Performer Inc. <cgary@cloudperformer.com>
* @version 1.0
* @group Opportunity Cloning
* @history 2021-04-02 - CJG - Created
* 2021-04-12 - CJG - Added setting to restrict child records shown to a particular List
* 2021-07-08 - CJG - removed functionality to clear out OpportunityLineItem.UnitPrice in
* order to insert OpportunityLineItems.
*/
public with sharing class OpportunityCloneService {
/**
* @description exception class that is thrown when something happens in the opportunity
* clonde process.
*/
public class CloneException extends System.Exception {}
/**
* @description Map of Child relationship objects to the opportunity.
*/
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
*/
static {
List<Schema.ChildRelationship> childRelationshipList =
Schema.Opportunity.getSObjectType().getDescribe().getChildRelationships();
opportunityChildrenMap = new Map<String, Schema.DescribeSObjectResult>();
List<String> restrictedRelationshipList = new List<String>();
String restrictedChildList = Label.SmartCloneChildList;
if(String.isNotBlank(restrictedChildList)){
restrictedRelationshipList = restrictedChildList.split(';');
}
for(Schema.ChildRelationship childRel:childRelationshipList) {
if(String.isNotBlank(childRel.getRelationshipName())){
Schema.DescribeSObjectResult childObjRes = childRel.getChildSObject().getDescribe();
if(!restrictedRelationshipList.isEmpty() && restrictedRelationshipList[0] != 'N/A'){
if(restrictedRelationshipList.contains(childObjRes.getName())){
opportunityChildrenMap.put(childRel.getRelationshipName(),childObjRes);
}
} else {
opportunityChildrenMap.put(childRel.getRelationshipName(),childObjRes);
}
}
}
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
* to an Opportunity that can be cloned.
* @return Map<String,String> Map where the key represents the relationship name and the value is the plural label
* of the child object.
*/
@AuraEnabled(cacheable=true)
public static Map<String,String> ObtainOpporunityChildRelationships(){
//return new List<String>(opportunityChildrenMap.keySet());
Map<String,String> relationshipMap = new Map<String,String>();
for(String key:opportunityChildrenMap.keySet()){
Schema.DescribeSObjectResult res = opportunityChildrenMap.get(key);
if(res.isAccessible() && res.isCreateable()){
String pluralLabel = res.getLabelPlural();
relationshipMap.put(key,pluralLabel);
}
}
return relationshipMap;
}
/**
* @description Lightning Exposed method that will provide a Map of fields in a fieldset on the
* opportunity.
* @param fieldsetName The name of the fieldset to use
* @return Map<String,Boolean> Map where the key is the apiFieldName and the value is whether the field is marked as
* 'required' in the fieldset.
*/
@AuraEnabled(cacheable=true)
public static Map<String, Boolean> ObtainCloneOpportunityFields(String fieldsetName){
Map<String,Boolean> fieldsToReturn = new Map<String, Boolean>();
Map<String,Schema.FieldSet> fieldSetMap = Schema.Opportunity.getSObjectType().getDescribe().fieldsets.getMap();
Schema.FieldSet fieldSetItem = fieldSetMap.get(fieldsetName);
if(fieldSetItem == null){
throw new OpportunityCloneService.CloneException('No field set named ' + fieldsetName + 'found on the Opportunity.');
}
for(Schema.FieldSetMember fieldMember:fieldSetMap.get(fieldsetName).getFields()) {
fieldsToReturn.put(fieldMember.getFieldPath(),fieldMember.getRequired());
}
return fieldsToReturn;
}
@AuraEnabled
public static Id CloneOpportunityWithChildren(Id oppId, Map<String,String> newValues,
List<String> childRelationshipsToClone) {
String oppFieldListToClearString = Label.SmartCloneForceClearFieldList;
List<String> oppFieldListToClear = new List<String>();
if(!oppFieldListToClearString.contains('N/A')){
oppFieldListToClear = oppFieldListToClearString.split(';');
}
//dynamic query in order to find the original opportunity.
String originalOppQueryString = ObtainQueryStringForOppAndChildren(oppId, childRelationshipsToClone);
System.debug(LoggingLevel.INFO,'\n\n*****\nQuery: ' + originalOppQueryString + '\n*****\n\n');
List<Opportunity> oppList = Database.query(originalOppQueryString);
//if the original opp cannot be found, throw and exception
if(oppList.isEmpty()){
throw new AuraHandledException('There was a problem obtaining original opportunity.');
}
Opportunity originalOpp = oppList[0];
//clone the original opportunity
Opportunity clonedOpp = originalOpp.clone(false,true,false,false);
//replace the values on the Opportunity with the new ones
for(String fieldName:newValues.keySet()){
clonedOpp.put(fieldName, CreateObjectValueFromString(fieldName,newValues.get(fieldName)));
}
//clear out field values listed
for(String fieldName:oppFieldListToClear) {
clonedOpp.put(fieldName,null);
}
//insert the new Opportunity
try{
insert clonedOpp;
} catch(System.Exception ex){
System.debug(LoggingLevel.INFO,'\n\n*****\nProblem creating new Opportunity: ' + ex.getMessage() + '\n*****\n\n');
throw new AuraHandledException('Unable to create new opportunity.');
}
//now we try to loop through the children and clone the records
List<Schema.ChildRelationship> childRelationships =
Schema.Opportunity.getSObjectType().getDescribe().getChildRelationships();
List<SObject> childRecsToInsert = new List<SObject>();
for(Schema.ChildRelationship childRel:childRelationships){
//if the child relationship name was passed in, get the records
if(childRelationshipsToClone.contains(childRel.getRelationshipName())){
List<SObject> childRecs = originalOpp.getSObjects(childRel.getRelationshipName());
childRecsToInsert.addAll(CloneAndReassociateRecords(childRecs,
childRel.getField().getDescribe().getName(),clonedOpp.Id));
}
}
if(!childRecsToInsert.isEmpty()){
try{
insert childRecsToInsert;
} catch(System.Exception ex) {
System.debug(LoggingLevel.INFO,'\n\n*****\nError cloning children: ' + ex.getMessage() + '\n*****\n\n');
throw new AuraHandledException('Unable to clone children.');
}
}
return clonedOpp.Id;
}
public static String ObtainQueryStringForOppAndChildren(Id oppRecordId, List<String> relationships) {
String queryString = 'SELECT ';
queryString += String.join(ObtainFieldsForObject('Opportunity'),',');
List<String> subqueryList = new List<String>();
for(String relationship:relationships){
Schema.DescribeSObjectResult childObject = opportunityChildrenMap.get(relationship);
String childQueryString = '(SELECT ' + String.join(ObtainFieldsForObject(childObject.getName()),',') + ' FROM ';
childQueryString += relationship + ')';
subqueryList.add(childQueryString);
}
if(!subqueryList.isEmpty()){
queryString += ', ' + String.join(subqueryList,',') + 'FROM Opportunity WHERE Id =\'' + oppRecordId + '\'';
} else {
queryString += ' FROM Opportunity WHERE Id =\'' + oppRecordId + '\'';
}
return queryString;
}
/**
* @description this method is responsible for getting the list of fields for a particular object
*/
private static List<String> ObtainFieldsForObject(String objectName) {
List<String> fieldNameList;
List<String> objectListToDescribe = new List<String>{objectName};
List<Schema.DescribeSObjectResult> describeItem = System.Schema.describeSObjects(objectListToDescribe);
if(describeItem.isEmpty()) {
throw new OpportunityCloneService.CloneException('Could not find info for ' + objectName + '.');
}
Map<String, Schema.SObjectField> fieldSObjectMapping = describeItem[0].fields.getMap();
fieldNameList = new List<String>();
for(Schema.SObjectField fieldToken:fieldSObjectMapping.values()) {
Schema.DescribeFieldResult fieldTokenDescribe = fieldToken.getDescribe();
if(fieldTokenDescribe.isCreateable() && fieldTokenDescribe.isUpdateable()) {
fieldNameList.add(fieldTokenDescribe.getName());
}
}
//need to account for weird objects where not all of the fields come back
if(objectName.toUpperCase() == 'OPPORTUNITYLINEITEM'){
fieldNameList.add('PricebookEntryId');
}
return fieldNameList;
}
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>();
for(SObject rec:recList){
SObject newRec = rec.clone(false,true,false,false);
newRec.put(parentFieldName, newParentId);
String sObjectName = newRec.getSObjectType().getDescribe().getName();
if(sObjectName.toUpperCase() == 'OPPORTUNITYLINEITEM') {
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);
}
}
return newList;
}
private static Object CreateObjectValueFromString(String fieldName, String fieldValue) {
Map<String,Schema.SObjectField> fieldSObjectMapping = Schema.Opportunity.getSObjectType().getDescribe().fields.getMap();
Schema.DisplayType fieldType = fieldSObjectMapping.get(fieldName).getDescribe().getType();
if(String.isBlank(fieldValue)){
return null;
}
Object returnObj;
switch on fieldType {
when ADDRESS, COMBOBOX, EMAIL, ENCRYPTEDSTRING, MULTIPICKLIST, PHONE, PICKLIST, STRING, TEXTAREA,URL {
returnObj = fieldValue;
}
when BOOLEAN {
returnObj = Boolean.valueOf(fieldValue);
}
when CURRENCY, DOUBLE, PERCENT {
returnObj = Double.valueOf(fieldValue);
}
when DATE {
returnObj = Date.valueOf(fieldValue);
}
when DATETIME {
returnObj = Datetime.valueOf(fieldValue);
}
when ID {
returnObj = Id.valueOf(fieldValue);
}
when INTEGER {
returnObj = Integer.valueOf(fieldValue);
}
when LONG {
returnObj = Long.valueOf(fieldValue);
}
when TIME {
returnObj = fieldValue;
}
when else {
returnObj = fieldValue;
}
}
return returnObj;
}
}