diff --git a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelper.cls b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelper.cls new file mode 100644 index 000000000..a5e684f00 --- /dev/null +++ b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelper.cls @@ -0,0 +1,162 @@ +public with sharing class InvocableApprovalHelper { + @InvocableMethod( + label='Approval Request (Action)' + description='Submit Approval Request Actions via Flow action.' + category='Approvals' + iconName='slds:standard:approval' + ) + public static List<Result> execute(List<Request> requests) { + List<Result> results = new List<Result>(); + List<Approval.ProcessWorkitemRequest> approvalWorkItems = new List<Approval.ProcessWorkitemRequest>(); + + // make ProcessWorkitemRequest from the submitted requests + for (Request curRequest : requests) { + Approval.ProcessWorkitemRequest approvalWorkItem = new Approval.ProcessWorkitemRequest(); + + approvalWorkItem.setAction(curRequest.action); + + if (String.isNotBlank(curRequest.comments)) { + approvalWorkItem.setComments(curRequest.comments); + } + + if (curRequest.nextApproverIds != null && !curRequest.nextApproverIds.isEmpty()) { + approvalWorkItem.setNextApproverIds(curRequest.nextApproverIds); + } + + approvalWorkItem.setWorkitemId( + String.isBlank(curRequest.approvalRequestId) + ? getApprovalRequestId(curRequest.recordId) + : curRequest.approvalRequestId + ); + + system.debug('approvalWorkItem ' + approvalWorkItem); + approvalWorkItems.add(approvalWorkItem); + } + + // process all requests at once (allOrNone = false errors will be returned to the flow) + List<Approval.ProcessResult> approvalResults = Approval.process(approvalWorkItems, false); + + // Process the results + Integer i = 0; + for (Request curRequest : requests) { + Approval.ProcessResult approvalResult = approvalResults[i]; + // Submit the request for approval + + system.debug('ApprovalResult ' + approvalResult); + + //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification) + results.add(new Result(approvalResult)); + i++; + } + + // return the results to the flow + return results; + } + + // create a string from the errors (if any) + public static String getErrorInfo(List<Database.Error> errors) { + if (errors == null || errors.isEmpty()) { + // no errors + return null; + } + + String errorStrings = 'There was an error processing the approval request:'; + for (Database.Error error : errors) { + errorStrings += '\n' + error.getMessage(); + } + + return errorStrings; + } + + public static Id getapprovalRequestId(String recordId) { + return new NoSharing().getapprovalRequestId(recordId); + } + + public without sharing class NoSharing { + public Id getapprovalRequestId(String recordId) { + String objName = ((Id) recordId).getSobjectType().getDescribe().getLocalName(); + String userId = UserInfo.getUserId(); + List<ProcessInstanceWorkitem> piws = [ + SELECT Id + FROM ProcessInstanceWorkitem + WHERE + ProcessInstance.TargetObjectId = :recordId + AND ProcessInstance.ProcessDefinition.Type = 'Approval' + AND ProcessInstance.ProcessDefinition.TableEnumOrId = :objName + AND ProcessInstance.Status = 'Pending' + AND ActorId = :UserInfo.getUserId() + LIMIT 1 + ]; + + // the user is allowed to approve/reject if the user is not a listed approver but has read all to the approval object + if (piws.isEmpty() && hasReadAllPermission(objName)) { + piws = [ + SELECT id + FROM ProcessInstanceWorkitem + WHERE + ProcessInstance.TargetObjectId = :recordId + AND ProcessInstance.ProcessDefinition.Type = 'Approval' + AND ProcessInstance.ProcessDefinition.TableEnumOrId = :objName + AND ProcessInstance.Status = 'Pending' + LIMIT 1 + ]; + } + return piws.isEmpty() ? null : piws[0].Id; + } + } + + public class Request { + @InvocableVariable( + label='1. Action, Valid values: \'Approve\', \'Reject\', \'Removed\' (Removed is for admins only)' + required=true + ) + public String action; // 'Approve', 'Reject'. 'Removed' https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_ProcessWorkitemRequest.htm#apex_Approval_ProcessWorkitemRequest_setAction + + @InvocableVariable(label='2. Record Id (The current Approval Request Id will be found for you)') + public String recordId; + + @InvocableVariable(label='3. Approval Request Id (use either this or the Record Id)') + public String approvalRequestId; + + @InvocableVariable(label='4. Comments') + public String comments; + + @InvocableVariable(label='5. Next Approver Ids') + public List<String> nextApproverIds; + } + + public class Result { + @InvocableVariable(label='Success') + public Boolean isSuccess; + + @InvocableVariable(label='Error') + public String errorString; + + @InvocableVariable( + label='Current Approval Process Status returns: \'Approved\', \'Rejected\', \'Removed\', \'Pending\')' + ) + public String currentApprovalProcessStatus; //Approved, Rejected, Removed or Pending. + + public Result(Approval.ProcessResult approvalResult) { + this.isSuccess = approvalResult.isSuccess(); + this.errorString = getErrorInfo(approvalResult.getErrors()); //warning. only hacking out the first error + this.currentApprovalProcessStatus = approvalResult.getInstanceStatus(); + } + } + + // report whether a given user has 'Read all' permission on a particular type of sObject + public static Boolean hasReadAllPermission(String sObjectName) { + return ![ + SELECT Id + FROM PermissionSetAssignment + WHERE + PermissionSetId IN ( + SELECT ParentId + FROM ObjectPermissions + WHERE SObjectType = :sObjectName AND PermissionsViewAllRecords = TRUE + ) + AND Assignee.Id = :UserInfo.getUserId() + ] + .isEmpty(); + } +} diff --git a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequest.cls-meta.xml b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelper.cls-meta.xml similarity index 80% rename from flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequest.cls-meta.xml rename to flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelper.cls-meta.xml index dd61d1f91..4b0bc9f38 100644 --- a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequest.cls-meta.xml +++ b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelper.cls-meta.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"> - <apiVersion>52.0</apiVersion> + <apiVersion>55.0</apiVersion> <status>Active</status> </ApexClass> diff --git a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelperTest.cls b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelperTest.cls new file mode 100644 index 000000000..d11eb3969 --- /dev/null +++ b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelperTest.cls @@ -0,0 +1,27 @@ +@isTest +public with sharing class InvocableApprovalHelperTest { + @isTest + public static void doApproveAction() { + Account acc = new Account(Name = 'testAcc'); + insert acc; + + // Create Request Object + List<InvocableApprovalHelper.Request> requests = new List<InvocableApprovalHelper.Request>(); + + InvocableApprovalHelper.Request req = new InvocableApprovalHelper.Request(); + req.action = 'Approve'; + req.comments = 'Test Comments'; + req.recordId = acc.Id; + req.nextApproverIds = new List<String>{ UserInfo.getUserId() }; + + // Add req to requests + requests.add(req); + + // Run Test + Test.startTest(); + List<InvocableApprovalHelper.Result> results = InvocableApprovalHelper.execute(requests); + Test.stopTest(); + + System.assertEquals(1, results.size(), 'there should be 1 result for 1 request'); + } +} diff --git a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelperTest.cls-meta.xml b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelperTest.cls-meta.xml new file mode 100644 index 000000000..4b0bc9f38 --- /dev/null +++ b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/InvocableApprovalHelperTest.cls-meta.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"> + <apiVersion>55.0</apiVersion> + <status>Active</status> +</ApexClass> diff --git a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequest.cls b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequest.cls deleted file mode 100644 index f6dc2b184..000000000 --- a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequest.cls +++ /dev/null @@ -1,100 +0,0 @@ -public with sharing class ResolveApprovalRequest { - @InvocableMethod - public static List<Results> execute(List<Requests> requests) { - List<Results> responseWrapper = new List<Results>(); - String approverId = ''; - for (Requests curRequest : requests) { - Results response = new Results(); - Approval.ProcessWorkitemRequest approvalWorkItem = new Approval.ProcessWorkitemRequest(); - approvalWorkItem.setComments(curRequest.comments); - approvalWorkItem.setAction(curRequest.action); - approvalWorkItem.setNextApproverIds(curRequest.nextApproverIds); - if(String.isBlank(curRequest.approvalRequestId)){ - approverId = getapprovalRequestID(curRequest.recordId,curRequest.ObjName).Id; - } - - approvalWorkItem.setWorkitemId(approverId); - - system.debug('approvalWorkItem ' + approvalWorkItem); - - // Submit the request for approval - Approval.ProcessResult[] approvalResult = Approval.process(approvalWorkItem); - - system.debug('ApprovalResult ' + approvalResult); - - // Verify the results - System.assert(approvalResult.isSuccess(), 'Result Status:'+ approvalResult.isSuccess()); - - System.assertEquals( - 'Approved', approvalResult.getInstanceStatus(), - 'Instance Status'+approvalResult.getInstanceStatus()); - - - response.isSuccess = approvalResult.isSuccess(); - Database.Error[] errors = approvalResult.getErrors(); - response.errorString = getErrorInfo(errors); //warning. only hacking out the first error - response.currentApprovalProcessStatus = approvalResult.getInstanceStatus(); - - //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification) - responseWrapper.add(response); - } - return responseWrapper; - - } - - public static String getErrorInfo(Database.Error[] errors) { - String errorStrings = ''; - if (errors != null) { - for(Database.Error error : errors) { - errorStrings = errorStrings + ' ' + error.getMessage(); - } - } - return errorStrings; - } - - public static ProcessInstanceWorkitem getapprovalRequestID (String recordId,String objName){ - return [select id,ActorId, CreatedDate, OriginalActorId,OriginalActor.name, ProcessInstanceId, ProcessInstance.Status, ProcessInstance.SubmittedById, ProcessInstance.ProcessDefinition.TableEnumOrId,ProcessInstance.ProcessDefinition.Type - from ProcessInstanceWorkitem where ProcessInstance.TargetObjectId =:recordId - and ProcessInstance.ProcessDefinition.Type ='Approval' - and ProcessInstance.ProcessDefinition.TableEnumOrId= :objName - and ProcessInstance.Status ='Pending' limit 1]; - } - - public class InvocableErrorException extends Exception { - } - - - public class Requests { - - @InvocableVariable - public String recordId; - - @InvocableVariable - public String objName; - - @InvocableVariable - public String approvalRequestId; - - @InvocableVariable - public String comments; - - @InvocableVariable - public String action; // 'Approved', 'Rejected'. 'Removed' https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_ProcessWorkitemRequest.htm#apex_Approval_ProcessWorkitemRequest_setAction - - @InvocableVariable - public List<String> nextApproverIds; - } - - public class Results { - - @InvocableVariable - public Boolean isSuccess; - - @InvocableVariable - public String errorString; - - @InvocableVariable - public String currentApprovalProcessStatus; //Approved, Rejected, Removed or Pending. - - } -} diff --git a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequestsTest.cls b/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequestsTest.cls deleted file mode 100644 index 66689e215..000000000 --- a/flow_action_components/ApprovalProcessActions/force-app/main/default/classes/ResolveApprovalRequestsTest.cls +++ /dev/null @@ -1,36 +0,0 @@ -@isTest -public with sharing class ResolveApprovalRequestsTest { - // Create New Request and Test Error Response - @isTest(SeeAllData=true) - public static void executeTest() { - // Get Current User Id - String userId = UserInfo.getUserId(); - - // Find Acme (Sample) Account - Account a = [SELECT Id FROM Account WHERE Name = 'Acme (Sample)']; - - // Find Process Definition - ProcessDefinition pd = [SELECT Id FROM ProcessDefinition WHERE DeveloperName = 'Test_Process_Definition']; - - // Find New Process Instance - ProcessInstance pi = [SELECT Id FROM ProcessInstance WHERE ProcessDefinitionId = :pd.Id LIMIT 1]; - - // Create Request Object - List<ResolveApprovalRequest.Requests> requests = new List<ResolveApprovalRequest.Requests>(); - ResolveApprovalRequest.Requests req = new ResolveApprovalRequest.Requests(); - req.action = 'Approve'; - req.comments = 'Test Comments'; - req.recordId = a.Id; - req.objName = 'Account'; - req.nextApproverIds = new List<String>{userId}; - - // Add req to requests - requests.add(req); - - // Run Test - Test.startTest(); - //ResolveApprovalRequest r = new ResolveApprovalRequest(); - ResolveApprovalRequest.execute(requests); - Test.stopTest(); - } -} \ No newline at end of file diff --git a/flow_action_components/ApprovalProcessActions/force-app/main/default/flows/Auto_Approve_Request.flow-meta.xml b/flow_action_components/ApprovalProcessActions/force-app/main/default/flows/Auto_Approve_Request.flow-meta.xml deleted file mode 100644 index 8171bef28..000000000 --- a/flow_action_components/ApprovalProcessActions/force-app/main/default/flows/Auto_Approve_Request.flow-meta.xml +++ /dev/null @@ -1,145 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<Flow xmlns="http://soap.sforce.com/2006/04/metadata"> - <actionCalls> - <name>Auto_Approve_Request</name> - <label>Auto Approve Request</label> - <locationX>176</locationX> - <locationY>398</locationY> - <actionName>ResolveApprovalRequest</actionName> - <actionType>apex</actionType> - <flowTransactionModel>CurrentTransaction</flowTransactionModel> - <inputParameters> - <name>action</name> - <value> - <stringValue>Approve</stringValue> - </value> - </inputParameters> - <inputParameters> - <name>approvalRequestId</name> - <value> - <elementReference>Extract_Approval_Request_ID.foundString</elementReference> - </value> - </inputParameters> - <storeOutputAutomatically>true</storeOutputAutomatically> - </actionCalls> - <actionCalls> - <name>Extract_Approval_Request_ID</name> - <label>Extract Approval Request ID</label> - <locationX>176</locationX> - <locationY>158</locationY> - <actionName>FindText</actionName> - <actionType>apex</actionType> - <connector> - <targetReference>Post_Incoming_Vars_to_Chatter</targetReference> - </connector> - <flowTransactionModel>CurrentTransaction</flowTransactionModel> - <inputParameters> - <name>inputString</name> - <value> - <elementReference>Email_TextBody</elementReference> - </value> - </inputParameters> - <inputParameters> - <name>length</name> - <value> - <stringValue>15</stringValue> - </value> - </inputParameters> - <inputParameters> - <name>prefix</name> - <value> - <stringValue>id=</stringValue> - </value> - </inputParameters> - <inputParameters> - <name>searchType</name> - <value> - <stringValue>prefixSearch</stringValue> - </value> - </inputParameters> - <storeOutputAutomatically>true</storeOutputAutomatically> - </actionCalls> - <actionCalls> - <name>Post_Incoming_Vars_to_Chatter</name> - <label>Post Incoming Vars to Chatter</label> - <locationX>176</locationX> - <locationY>278</locationY> - <actionName>chatterPost</actionName> - <actionType>chatterPost</actionType> - <connector> - <targetReference>Auto_Approve_Request</targetReference> - </connector> - <flowTransactionModel>CurrentTransaction</flowTransactionModel> - <inputParameters> - <name>text</name> - <value> - <elementReference>StatusInfo</elementReference> - </value> - </inputParameters> - <inputParameters> - <name>subjectNameOrId</name> - <value> - <elementReference>$User.Username</elementReference> - </value> - </inputParameters> - <inputParameters> - <name>type</name> - <value> - <stringValue>User</stringValue> - </value> - </inputParameters> - <storeOutputAutomatically>true</storeOutputAutomatically> - </actionCalls> - <apiVersion>53.0</apiVersion> - <interviewLabel>Auto Approve Request {!$Flow.CurrentDateTime}</interviewLabel> - <label>Auto Approve Request</label> - <processMetadataValues> - <name>BuilderType</name> - <value> - <stringValue>LightningFlowBuilder</stringValue> - </value> - </processMetadataValues> - <processMetadataValues> - <name>CanvasMode</name> - <value> - <stringValue>AUTO_LAYOUT_CANVAS</stringValue> - </value> - </processMetadataValues> - <processMetadataValues> - <name>OriginBuilderType</name> - <value> - <stringValue>LightningFlowBuilder</stringValue> - </value> - </processMetadataValues> - <processType>AutoLaunchedFlow</processType> - <start> - <locationX>50</locationX> - <locationY>0</locationY> - <connector> - <targetReference>Extract_Approval_Request_ID</targetReference> - </connector> - </start> - <status>Draft</status> - <textTemplates> - <name>StatusInfo</name> - <isViewedAsPlainText>true</isViewedAsPlainText> - <text>Email Text Body: {!Email_TextBody} -Email HTML Body: {!Email_HTMLBody} - -extracted id = {!Extract_Approval_Request_ID.foundString}</text> - </textTemplates> - <variables> - <name>Email_HTMLBody</name> - <dataType>String</dataType> - <isCollection>false</isCollection> - <isInput>true</isInput> - <isOutput>false</isOutput> - </variables> - <variables> - <name>Email_TextBody</name> - <dataType>String</dataType> - <isCollection>false</isCollection> - <isInput>true</isInput> - <isOutput>false</isOutput> - </variables> -</Flow>