Apex code to enable dynamic approval routing

Many of you are probably using Apex code to implement sophisticated logic, such as round-robin assignment, custom quote generation, etc. Did you know that you can now use Apex code to dynamically route approval requests to different users within your organization?

You can accomplish this by writing Apex that populates the values of user lookup fields on records that must be approved, and using a new feature in the approval step configuration that allows you to route approval requests to a user specified in any user lookup field on the submitted record. This creates an approval process in which the approvers are selected dynamically.

A single dynamic approval process can do the work of several non-dynamic approval processes. Having less approval processes to manage will increase the productivity of administrators. You can learn more about building dynamic approval processes by downloading and installing the dynamic approval process sample solution from the AppExchange. The sample solution is ready-to-use, and demonstrates how different features of the Force.com platform come together to enable dynamic approval routing. It also includes examples of bulk triggers, as well as several tests so you can do test-based development.


Test driven development

The sample solution includes several tests that result in a 100% code coverage. The sample solution includes tests for both single row and bulk processing, as well as a test to verify that all critical validation rules are enabled. The following is a snippet from one of the test methods.

    public static testmethod void validationTest() {
        Approval_Routing_Rule__c rule = new Approval_Routing_Rule__c();
        try {
            Database.insert(rule);
            System.assert(false,'Insert did not throw expected exception: FIELD_CUSTOM_VALIDATION_EXCEPTION with null values for account_type__c and owner_region__c.');
        } catch(System.DmlException e) {
            System.assertEquals(StatusCode.FIELD_CUSTOM_VALIDATION_EXCEPTION, e.getDmlType(0));  
        }  
        ...
      }  


Bulk Triggers

The following is a snippet of Apex included in the sample solution, which shows how to effectively use the Set and Map data structures to code for bulk triggers and ensure that you are within the governor limits.

Note: This is just a snippet from the apex code included in the package

 /* The map of accountId -> List<Opportunity>.
    opporutnities passed in with null accountId values are ignored.
   */
        Map<Id, List<Opportunity>> accountToOpportunitiesMap = new Map<Id, List<Opportunity>>();
        
        /* The set of user(owner) Ids that represent the unique owners of the given opportunities. */
        Set<Id> userIdSet = new Set<Id>();
        
        /* Loop through the opportunities, adding to the map and set */
        for(Opportunity o:opportunities) {
            
            Id aid = o.accountId;
            userIdSet.add(o.ownerId);
            
            /* if the account Id is null we ignore it */
            if(aid != null) {
              
              /* if there is no list for this account id, create it */
              if(accountToOpportunitiesMap.get(aid) == null) accountToOpportunitiesMap.put(aid, new List<Opportunity>());
              
              /* add the opportunity to the map under this account id.*/
              accountToOpportunitiesMap.get(aid).add(o);
            }
        }
       ...
       ...

The package may be downloaded here: Dynamic Approval Routing 1.0.


Send approval requests to the delegated approver

The current approvals engine does not route approval requests or emails to the delegated approvers. You can now send approval requests to the delegated approver by using the Dynamic Approval Routing feature.

You can achieve this by creating couple of custom fields on the User Object . For example, Vacation Start Date and Vacation End Date and use apex code to send to the delegate instead of the approver. Each user can set the vacation start date, end date and the delegated user by updating his profile.

The following instructions are can be used to enhance the Apex class packaged in the Dynamic Approval Routing 1.0 package to send approval requests to the delegated approver.

  1. Create a new Set to hold the unique IDs of all the dynamic approvers
  2. Populate the set when you are assigning the approvers to respective fields on the opportunity objec t
  3. Create a new Map to hold the ID of the delegated approver for each of the actual approver
  4. Query the User record passing in the Set of actual approver to find the delegated approver and populate the Map
  5. Loop through the list of Opportunities and replace the actual approver by the delegated approver.


The following is a snippet of Apex code which will send the approval request to the delegated approver if the main approver is on vacation:

...
...
/* Set to capture the unique set of approvers - This will be used to determine delegated approvers*/		
     Set<Id> approverUserIdSet = new Set<Id>();
		
/* Iterate over the approval_routing_rule__c objects with the respective routing key values
so we can set the appropriate level assignments on the opportunities. */
	for(Approval_Routing_Rule__c rule:[select routing_key__c, Level1__c, Level2__c, Level3__c 
		                                     from Approval_Routing_Rule__c 
		                                     where routing_key__c in :routingKeyToOpportunitiesMap.keySet()]){
		                                     	
         	for(Opportunity o:routingKeyToOpportunitiesMap.get(rule.routing_key__c)) {
         		o.level1__c = rule.level1__c;
         		o.level2__c = rule.level2__c;
         		o.level3__c = rule.level3__c;
         		approverUserIdSet.add(rule.level1__c);
         		approverUserIdSet.add(rule.level2__c);
         		approverUserIdSet.add(rule.level3__c);
         	}	
	}
		

/* This Map stores the Delegated Approver ID for any approver if that person is on vacation*/	
	Map<Id, Id> delegatedApproversMap = new Map<Id, Id>();

/* This assumes that the User object has 2 custom Date fields with API Names: Vacation_Start_Date__c and Vacation_End_Date__c */
		for(User approvers:[select Id, Vacation_End_Date__c, Vacation_Start_Date__c, DelegatedApproverId
									from User
									where id in:approverUserIdSet
									and Vacation_End_Date__c <> null
									and Vacation_End_Date__c <> null
									and DelegatedApproverId <> null]) {
			if (approvers.Vacation_Start_date__c < System.today() && approvers.Vacation_End_date__C > System.today()) {
				if(delegatedApproversMap.get(approvers.Id) == null) delegatedApproversMap.put(approvers.Id, approvers.DelegatedApproverId);
			}
		}
		
/* Overwrite the approver ID with the delegated approver */
		String anID;
		for(Opportunity o:opportunities) {
			anID = delegatedApproversMap.get(o.level1__c);
			if(anId != null) o.level1__c = anId;
			anID = delegatedApproversMap.get(o.level2__c);
			if(anId != null) o.level2__c = anId;
			anID = delegatedApproversMap.get(o.level3__c);
			if(anId != null) o.level3__c = anId;				
		}
...
...