Apex Approval Actions

Approval Processes are a great feature of the Salesforce platform. They stand firmly behind the ‘No Software’ banner by giving ‘Power Users’ the ability to configure approval criteria and outcomes via a point and click interface. Aproval outcomes can create tasks, update fields, send emails or even send outbound messages to another system. What’s not to like? One drawback, however, is that quite often you’ll find that Approval Processes are created and maintained by ‘Power Users’ rather than developers. ‘Power Users’ often have an intimate knowledge of the business processes but little or no knowledge of the code base. So, when an Approval Process requires a more complex outcome it may be that a developer needs to get involved and the ‘No Software’ premise falls apart. When working on the Salesforce platform in cases like this I prefer to think about how better to empower users. In the case of Approval Processes one way of providing this power is to offer them a suite of approval actions that they can easily plug into their own process flows. To achieve this, I’m going to show you a trigger based pattern that uses a custom picklist field to invoke a custom action. When a user is creating their Approval Process (or even a workflow for that matter!) they can use a field update to run a specific action against the object. If you’re working in a managed package this approach is extensible allowing you to package the framework along with a standard suite of Approval Actions and then let your customers implement their own. For this example let’s consider a couple of useful actions we may want to perform when approving or rejecting an Opportunity. If an Opportunity is approved we may want to record a Sale against the Opportunity owners sales target. If an Opportunity is rejected we may want to add the Opportunity to a list of items that require further review at the next weekly sales meeting. The list could go on, you could raise an invoice, produce and email a statement of works etc etc. We’ll begin this approach by defining a global interface that all Approval Action classes must implement.

/**
 * Global Interface for Approval Actions
 */
global Interface IApprovalAction
{
	/**
	 * Execute method.
	 *
	 * Arguments:	SObject[] - Array of SObjects to process
	 *		String - Field containing the action class name
	 */
	void execute(SObject[] sobs, String classLocatorField);

	/**
	 * Validate Type
	 *
	 * Arguments: 	SObjectType - The type of the Object being passed
	 *
	 * Returns:	Boolean - true if the supplied type is correct
	 */
	Boolean validateType(SObjectType soType);
}

The interface contains two methods. The execute() method takes a payload of SObjects to perform an action against. Bear in mind this logic is being handled in the context of a trigger and like all trigger code it should be designed to handle bulk operations. For that reason our interface accepts a list of SObjects rather than a single SObject even though approvals tend to work on a single record at a time. There’s nothing to prevent this pattern being called in other trigger contexts so for safety we’ll keep that in mind. The validateType() method is used to ensure the action is compatible with the object passed to it. We’re going to use a Picklist field with all the available approval action class names on our target object, in this case the Opportunity object. Opportunity Custom Field Now class names aren’t always very user friendly so consider the names carefully. Be sure to add the namespace prefixes to them if applicable. Tip: You can localise picklist values if you want to make it super clear as long as the actual value matches your class names. In our example we need two approval action classes, one called RecordSaleAction and one called AddToReviewListAction. The RecordSaleAction class implements the interface and adds a record to the Sale__c object:

/**
 * Approval action for recording a sales against an Opportunity
 */
public without sharing class RecordSaleAction
	implements IApprovalAction
{
	/**
	 * Constructor
	 */
	public RecordSaleAction()
	{
	}

	/**
	 * Execute method (defined by interface)
	 *
	 * This method should be bulkified since it deals with an array of SObjects.
	 *
	 * Arguments:	SObject[] - Array of SObjects to process
	 *		String - Field containing the action class name (which needs to be cleared to prevent actions refiring)
	 */
	public void execute(SObject[] sobs, String classLocatorField)
	{
		// List of Sales
		List<Sale__c> sales = new List<Sale__c>();

		// Loop through the SObjects
		for (SObject so : sobs)
		{
			// cast to a concrete type
			Opportunity opp = (Opportunity)so;

			// add a new Sale to the list
			sales.add(new Sale__c(
				Opportunity__c = opp.Id,
				Salesman__c = opp.OwnerId,
				SaleValue__c = opp.Amount
			));

			// reset the approval action field
			opp.put(classLocatorField, null);
		}

		// insert the Sale__c records and update the Opportunities
		insert sales;
		update sobs;
	}

	/**
	 * Validate the SObject type this action supports.
	 *
	 * Arguments: 	SObjectType - The type of the Object being passed
	 *
	 * Returns:	Boolean - true if the supplied type is correct
	 */
	public Boolean validateType(SObjectType soType)
	{
		// Must be an Opportunity
		if (soType == Opportunity.SObjectType)
		{
			return true;
		}

		return false;
	}
}

The Sale__c object looks like this:

Field Label Type Properties
Id Record ID id
Name Sales Id text(80)
SaleValue__c Sale Value currency(16,2)
Salesman__c Salesman foreign key User
Opportunity__c Opportunity foreign key Opportunity

Similarly the AddToReviewListAction class also implements the interface but adds a record to the PendingReview__c object:

/**
 * Approval action for adding an Opportunity to the review list
 */
public without sharing class AddToReviewListAction
	implements IApprovalAction
{
	/**
	 * Constructor
	 */
	public AddToReviewListAction()
	{
	}

	/**
	 * Execute method (defined by interface)
	 *
	 * This method should be bulkified since it deals with an array of SObjects.
	 *
	 * Arguments:	SObject[] - Array of SObjects to process
	 *		String - Field containing the action class name (which needs to be cleared to prevent actions refiring)
	 */
	public void execute(SObject[] sobs, String classLocatorField)
	{
		// List of Reviews
		List<PendingReview__c> reviews = new List<PendingReview__c>();

		// Loop through the SObjects
		for (SObject so : sobs)
		{
			// cast to a concrete type
			Opportunity opp = (Opportunity)so;

			// add a new PendingReview to the list
			reviews.add(new PendingReview__c(
				Opportunity__c = opp.Id
			));

			// reset the approval action field
			opp.put(classLocatorField, null);
		}

		// insert the PendingReview__c records and update the Opportunities
		insert reviews;
		update sobs;
	}

	/**
	 * Validate the SObject type this action supports.
	 *
	 * Arguments: 	SObjectType - The type of the Object being passed
	 *
	 * Returns:	Boolean - true if the supplied type is correct
	 */
	public Boolean validateType(SObjectType soType)
	{
		// Must be an Opportunity
		if (soType == Opportunity.SObjectType)
		{
			return true;
		}

		return false;
	}
}

The PendingReview__c object looks like this:

Field Label Type Properties
Id Record ID id
Name Review Id text(80)
Opportunity__c Opportunity foreign key Opportunity

Note that the Action classes should ensure they reset the Action field to null on completion so that another update against the record does not cause the action to be fired again. I could have added this logic to the handler but I wanted to keep it flexible enough so that if needed the Action could execute asynchronous code if required. To hook up the Action classes to the triggers we have a generic handler class that sorts the appropriate actions to execute and delegates them accordingly. As we are dealing with a trigger the handler needs to be able to handle a bulk operation not just one record at a time. The handler looks like this:

/**
 * Factory class for resolving and executing Approval Actions
 */
global without sharing class ApprovalActionHandler
{
	/**
	 * Static handler method to execute the appropriate action
	 * for the given SObjects.
	 *
	 * Arguments:	String - Class locator field to determine the class to call.
	 */
	global static void handle(String classLocatorField)
	{
		// This handler should only be executed in an after update trigger context
		if (!(Trigger.isUpdate && Trigger.isAfter))
		{
			return;
		}

		// if no locator provided, quit now
		if (classLocatorField == null ||
			classLocatorField == '')
		{
			return;
		}

		// Get the objects to process from the Trigger context
		SObject[] sobs = Trigger.new;

		// Create a map of Class Name to Action
		Map<String, Action> actionMap = new Map<String, Action>();

		// iterate the sobjects, populating the map
		for (SObject so : sobs)
		{
			// get the class name from the class locator field
			String className = (String)so.get(classLocatorField);

			// if we have a class set
			if (className != null && className != '')
			{
				// get the action from the map
				Action myAction = actionMap.get(className);

				// if there is no action, construct it and add it to the map
				if (myAction == null)
				{
					IApprovalAction implementation = actionResolver(className);

					myAction = new Action(implementation);
					actionMap.put(className, myAction);
				}

				// validate the object is suitable for the implementation
				if (!myAction.Implementation.validateType(so.getSObjectType()))
				{
					throw new HandlerException('Action class: ' + className + ' does not support Object Type: ' + so.getSObjectType());
				}

				// store a clone of the object in the payload (this is because we are in the after trigger and the object is now read only)
				myAction.Payload.add(so.clone(true, true));
			}
		}

		// loop through the action map values and execute the actions
		for (Action myAction : actionMap.values())
		{
			// execute the action
			myAction.execute(classLocatorField);
		}
	}

	/**
	 * Action resolver method.
	 *
	 * Arguments:	String - Class name to resolve
	 *
	 * Returns:	IApprovalAction
	 */
	private static IApprovalAction actionResolver(String className)
	{
		Type t = Type.forName(className);

		// We can't find a type, throw an exception
		if (t == null)
		{
			throw new HandlerException('Could not resolve action: ' + className);
		}

		// Instantiate the type
		Object o = t.newInstance();

		// if its not an instance of IApprovalAction, throw an exception
		if (!(o instanceOf IApprovalAction))
		{
			throw new HandlerException('Class: ' + className + ' does not implement IApprovalAction.');
		}

		return (IApprovalAction)o;
	}

	/**
	 * Inner class to wrap an actions implementation and payload.
	 */
	private class Action
	{
		public IApprovalAction Implementation {get; set;}
		public List<SObject> Payload {get; set;}

		public Action(IApprovalAction implementation)
		{
			this.Implementation = implementation;
			this.Payload = new List<SObject>();
		}

		public void execute(String classLocatorField)
		{
			Implementation.execute(Payload, classLocatorField);
		}
	}

	// Exception class
	global class HandlerException extends Exception {}
}

You’ll notice that the SObjects are cloned before being added to the Action classes payload. This is because our trigger is operating on the After Update so as not to intefere with any other trigger processing. At this point in the trigger the SObject is read only. Cloning it effectively releases it from its read only state and allows the Action class to fire a second update in order to clear its action state once the action has been completed.

To complete the code we add a simple trigger to the Opportunity. This is to invoke the handler and pass in the API name of the picklist containing the Action class to invoke:

/**
 * Trigger to invoke the Approval Actions
 */
trigger OpportunityApprovalActionTrigger on Opportunity (after update)
{
	ApprovalActionHandler.handle('ApprovalAction__c');
}

Ok, so all that remains is to throw it into the hands our ‘Power Users’ to add it into their Approval Processes then it’s job done: Approval Process

Field Update 1

Field Update 2

Advertisements

Author, brainstormer, coder, dad, explorer, four chord trickster, gig goer, home worker, inquisitor, joker, knowledge seeker, likes: marmite, note scribbler, opinionator, poet, quite likes converse, roller skater, six music listener, tea drinker, urban dweller, vinyl spinner, word wrangler, x-factor hater, Yorkshireman (honorary), zombie slayer (lie).

Tagged with: , , ,
Posted in apex, code, force.com, salesforce
One comment on “Apex Approval Actions

Comments are closed.

About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.

Enter your email address to follow this blog and receive notifications of new posts by email.

Copyright (there isn’t any, feel free to reuse!)

CC0
To the extent possible under law, Tony Scott has waived all copyright and related or neighboring rights to MeltedWires.com Examples and Code Samples. This work is published from: United Kingdom.

%d bloggers like this: