Custom Approval Process: A New Perspective, Pavel Hrbacek & Anindya Halder

CzechDreamin 41 views 19 slides May 23, 2024
Slide 1
Slide 1 of 19
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19

About This Presentation

The standard Salesforce Approval process can be limiting in many ways, especially in complex scenarios.

What if there was a way to implement very flexible approvals where one can use Apex code to make data updates in unrelated records, dynamically generate next steps details, and compute assignees ...


Slide Content

Custom Approval Process: A Different Perspective Pavel Hrbáček Anindya Hader

Why are we here today? Our client had very complex and not finite requirements for Approval processes Dynamic rules for approving e.g. 2 of 3 approvals needed based on the context record details Make data updates in unrelated records Compute assignees on the fly Approval with a change (Amendment) Dynamically generate next Approval requests Pause/ Unpause active approval process Highly configurable

This presentation outlines an idea for very customisable approvals framework addressing the limitations of the standard approval process. The proposed solution leverages Apex code stored within text fields and a custom implementation of the EVAL method for Apex to provide dynamic approval/rejection criteria and automate process progression, reducing the configuration overhead. Seamlessly handles very complex custom scenarios such as determining completion based on a specified percentage or number of approvals . The Assigned Approvers can be computed dynamically at the time of the task generation. Highly configurable – ideal for Consultants with some Apex knowledge Approach we took

ERD Approach Process Definition ( ContextObject , EntryCriteria ) Process Node ( StepId , EntryCriteria ) Process Node ( StepId , EntryCriteria )

ERD Approach ProcessInstance ( ContextObjectRecordId , Status) ProcessInstanceNode ( StepStatus , CompletionDate ) ProcessInstanceNode ( StepStatus , Completiondate ) ProcessInstanceWorkItem   (Actor, Status) ProcessInstanceWorkItem   (Actor, Status) ProcessInstanceWorkItem   (Actor, Status)

ERD

eval in Apex It should be noted that using executeAnonymous won't execute in the same context the way the Javascript   eval()  does. Any inputs need to be explicitly included in the Apex string to execute.  Also, any return values need to be returned via the log and then parsed out to bring them into the context of the calling Apex. Programmatic evaluation of Apex string and extraction of the result using  executeAnonymous API call  - ExecuteAnonymous call is available through Apex API  &  Tooling API . We use the Apex API – we can add  debugging header ( < apex:DebuggingHeader > ) in request to get log in response and then parse that response to get the result. Build up the anonymous Apex string including any required inputs. Use a  System.debug ( LoggingLevel.Error , 'output here')  to send back the output data. Call the Apex API  executeAnonymous   web method  Capture the  DebuggingInfo  SOAP header in the response and Parse the  USER_DEBUG Error message  out of the Apex Log.   Convert the resulting string to the target data type if required.

Framework eval() Core methods Public methods Workflow configuration Public methods   submit( processDefinition,contextRecord ) {  String errorMessage =  WF_helper.checkEntryCriteria ( processDefinition , contextRecord );  If( String.isBlank ( errorMessage )) { //start process instance, compute first stepid , activate node with that stepid and create workitems /approval requests} } Workflow Core methods String  contextRecordString  = 'Account record = [SELECT Id, Industry FROM Account WHERE Id =\' '+ contextRecord.Id + '\'];'; String configScript  =  processDefinition. EntryCriteria __c; String toEval = contextRecordString ; toEval += 'String result = \'\';'; toEval += configScript ; toEval += ' System.debug ( LoggingLevelError , result);'; String finalResult = WF_ExecuteAnonymousApex.eval ( toEval ); return finalResult ; Final String with code used by eval() Account record = [SELECT Id, Industry FROM Account WHERE Id ='< accountId >']; String result =  record.Industry  == 'Finance' ? '':'Criteria doesn't match'; System.debug ( LoggingLevel.Error , result); Workflow Configuration processDefinition's   EntryCriteria __c field value is the below string   result =  record.Industry  == 'Finance' ? '':'Criteria doesn't match';

Framework ExecuteAnonymous API call   String endpoint = URL.getSalesforceBaseUrl (). toExternalForm () + '/services/Soap/s/56.0';    HttpRequest req = new HttpRequest ();   req.setEndpoint ( endpoint_x );   req.setMethod ('POST');   req.setHeader ('Content-Type', 'text/xml; charset=UTF-8');   req.setHeader (' SOAPAction ', 'blank');   req.setBodyDocument (doc);   Http http = new Http();   HTTPResponse res = http.send (req);     return extractDebugLog (res.getBodyDocument()); Request Body < soapenv:Envelope      xmlns:soapenv ="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">    < soapenv:Header>       < apex:DebuggingHeader>          < apex:categories>             < apex:category > Apex_code </ apex:category>             < apex:level >ERROR</ apex:level>          </ apex:categories>          < apex:debugLevel >NONE</ apex:debugLevel>       </ apex:DebuggingHeader>       < apex:SessionHeader>          < apex:sessionId > Session Id </ apex:sessionId>       </ apex:SessionHeader>    </ soapenv:Header>    < soapenv:Body>       < apex:executeAnonymous>          < apex:String > Final string of code; </ apex:String>       </ apex:executeAnonymous>    </ soapenv:Body> </ soapenv:Envelope > Final String with code Account record = [SELECT Id, Industry FROM Account WHERE Id ='< accountId >']; String result =  record.Industry  == 'Finance' ? '':'Criteria doesn't match'; System.debug ( LoggingLevel.Error , result); Response < soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/..."     < soapenv:Header>       < DebuggingInfo>          < debugLog>31.0 APEX_CODE,ERROR Execute Anonymous: Final string of code
13:24:24.027 (27564504)|EXECUTION_STARTED
13:24:24.027 (27573409)|CODE_UNIT_STARTED|[EXTERNAL]| execute_anonymous_apex 13:24:24.028 (28065096)|USER_DEBUG|[1]|ERROR| Actual Output ("" or C riteria doesn't match )   13:24:24.028 (28098385)| CODE_UNIT_FINISHED|execute_anonymous_apex 13:24:24.029 (29024086)|EXECUTION_FINISHED</ debugLog>       </ DebuggingInfo>    </ soapenv:Header>    < soapenv:Body>       < executeAnonymousResponse>          <result>
            <column>-1</column>
            < compileProblem xsi:nil="true"/>             <compiled>true</compiled>
            < exceptionMessage xsi:nil="true"/>             < exceptionStackTrace xsi:nil="true"/>             <line>-1</line>
            <success>true</success>
         </result>
      </ executeAnonymousResponse>    </ soapenv:Body> </ soapenv:Envelope >

Workfow Configuration

Process Configuration Samples Scenario Developers which are employees of a company and they have been contracted to work on a project for a client. When they want to go on an annual leave, they need to get an approval from both, their own direct managers (Line, Delivery and Division Manager) and the client's project manager. Rules At least 50% of their Employer direct Managers needs to approve, e.g. 2 of 3 managers. Once 50% was achieved the approval is given and the process can continue. Then a Project Manager on the Client side needs to approve as well, but only if the Developer is currently assigned to a project.

Process Execution - Submission

AFTER SUBMISSION ACTION After Submission Sample Map<String, List<User>> roleToUsersMap = WF_Actors.getRoleUsers ( contextRecordId ,
  new List<String> {'Line Manager', 'Delivery Manager', 'Division Manager'});

List< WF_App_Actions.RequestParams > requestParamsList = new List< WF_App_Actions.RequestParams >(); for (String role : roleToUsersMap.keySet ()) {
  for (User assignee : roleToUsersMap.get (role)) {
      requestParamsList.add (new WF_App_Actions.RequestParams (
        'Approve - ' + role,
        'Select \'Approve\' if you are happy to approve the leave.',
        assignee, role, null));
    }   
  }
} WF_App_Actions.createApproverRequest ( processInstanceId , processInstanceNodeId , requestParamsList );

IS FINAL APPROVAL – FRAMEWORK CODE After Approval Sample public static Boolean checkIfFinalApproval (
  Id processInstanceNodeId ,
  Integer percentageRequired ) completedApprovals = Integer.valueOf ( processInstanceNode. NumberOfApprovals __c); Integer allApprovals = [SELECT Count()     FROM WF_ ProcessInstanceWorkItem __c      WHERE processInstanceNode =: processInstanceNodeId ]; return ( completedApprovals / allApprovals *100) >= percentageRequired ; // When 50% percentage required result = WF_App_Actions.checkIfFinalApproval ( processInstanceNodeId , 50);  result = ourHR.hasActiveProject (contextRecord. RequestedBy __c ? '002' : null; APPROVED NEXT STEP ID Map<String, List<User>> roleToUsersMap = LeaveApproval.getRoleUsers ( contextRecordId ,   new List<String> {'Project Manager'}); If ( roleToUsersMap.size () == 0) return; // Finish the step when user has no Project Manager List< WF_App_Actions.RequestParams > requestParamsList =    new List< WF_App_Actions.RequestParams >();

for (String role : roleToUsersMap.keySet ()) {
  for (User assignee : roleToUsersMap.get (role)) {     requestParamsList.add (new WF_App_Actions.RequestParams (      'Approve a leave request', 'Select \'Approve\' if you are happy to approve,        assignee, role, null, null, false))
    }
  } WF_App_Actions.createApproverRequest ( processInstanceId , nextProcessInstanceNodeId , requestParamsList ); AFTER FINAL APPROVAL ACTION IS FINAL APPROVAL - CONFIGURATION

Implementation highlights Topic Solution Approach Executing anonymous apex script runs as the running user, not in system mode, and if the script need to refer to any Apex classes to invoke methods, we would need to grant the users permissions that we wouldn’t want to, such as Author Apex or access to Setup Using Named Credential to authenticate and login as an admin user. With Named Credentials, we can provide the callout with admin credentials so the dynamic code can be executed with as much flexibility as our written code. The Named Credential takes in the URL of the callout we want to make and the necessary authentication credentials for a User. When the callout is being made, those credentials are used to authenticate and perform the callout. Executing anonymous Apex script relies on making a callout, and we need to avoid the error of ‘uncommitted changes in transaction…' whenever trying to make a callout after any DMLs within the same transaction We execute the Apex script in a future call.  Configuration details -  All configs are saved in custom object records like ProcessDef , Process Node etc.  We need a way to migrate these records across orgs and as well make them available for unit tests Fetch the records and save them as CSVs and upload them as static resources. The static resources can be migrated from one org to another using source control system. We can then have a command line data loader or an Apex class to read static resources and load records.

Q&A

Our article about the Approvals Custom Approval Process: A Different Perspective | Bluewave (bluewavecx.com) - https://bit.ly/44HalH4 Thanks to Kevin Poorman https://codefriar.wordpress.com/2014/10/30/eval-in-apex-secure-dynamic-code-evaluation-on-the-salesforce1-platform/ And  Daniel Ballinger https:// www.fishofprey.com /2014/11/adding-eval-support-to-apex.html Resources