A Business Use Case for Batch Apex

I want to share a recent real-world use case where Batch Apex was used to achieve our goal.  If you’re not familiar with Batch Apex you can find more here in the Salesforce Development Docs.

Business Use Case

An organization’s sales team is using Salesforce CRM primarily to manage their Leads, Contacts, and opportunities. This is akin to a retail setting whereby there are multiple physical locations and each location has its own sales team and sales manager (think car dealership).  In this setting, Leads (and/or Contacts) enter through multiple channels such as internet marketing, phone calls, walking through the front door, etc and are assigned to a sales rep based on a round-robin format.  That sales rep becomes the owner of the Lead, but only has protection for a certain period of time before the Lead is redistributed.  The protected period of time is based on the sales rep’s last activity to the Lead/Contact.  In other words, if the rep is not actively working the Lead/Contact then they lose protection, and if that Lead/Contact ends up buying a product then the rep will not be credited for the commission.

The question then became, how can we automate this process of removing Lead protection based on the sales rep’s activity?  One of the great features of working with the Force.com platform is that there are always multiple ways to solve these types of problems.  And these are good problems to solve because the solutions free up people’s time and help advance business.  In this situation, the basic design for this scenario is that when the number of days since the last activity is greater than 5 days, revert the ownership of the Lead/Contact from the sales rep to the sales manager.  This lets the sales manager redistribute the Lead/Contact to another sales rep.

Make It Happen

The first step was to create a new formula field that returns a Number and call it Days_Since_ Last_Activity.  The formula is this: Today() – LastActivityDate.  At this point, you might think about creating a Workflow Rule that says when Days_Since_Last_Activity is greater or equal to 5 then Update the Owner field.  However, a Workflow Rule is only fired under three circumstances: a)Only when a record is created; b) Every time a record is created or edited or; c) When a record is edited and did not previously meet the criteria.  This means that the workflow rule won’t get fired until the record gets edited, and we want the ownership to change immediately upon the passing of the protected period regardless of whether or not someone edits the record.

The solution in this case is to use Batch Apex to query the database for all Leads/Contacts (we use Contacts in the example code) that have crossed the protected period, i.e. the Days_Since_Last_Activity fields is greater than 5.  Then, we reassign ownership to the correct sales manager based on which physical location this Contact is associated with.  Next, we create a new Task associated with this Contact so that the Days_Since_Last_Activity gets reset.  Lastly, we schedule this Batch Apex to run each night so that the ownership is being recalculated on a daily basis.

Below are the Apex and Test Classes that make this work.


Batch Apex Class:

global class UpdateAllContacts implements Database.Batchable<sObject>{

	//This is the query that is passed to the execute method.  It queries all of the Contacts who have passed
	//the protected period.
	String query = 'Select Id, Club_Location__C, OwnerId FROM Contact WHERE Days_Since_Last_Activity__c > 5';

	global database.queryLocator start(Database.BatchableContext BC) {
		return database.getQueryLocator(query);

	} //close start method

	global void execute(Database.BatchableContext BC, list <Contact> scope) {

		List <Task> taskList = new List<Task>();

		// Iterate through the whole query of Contacts and transfer ownership based on the location.
		// This example only has two locations.
		// Create a Task that's associated with each Contact.  This resets the Days Since Last Activity formula field.
		for(Contact c : scope) {
			if(c.Location__c == 'Location 1') {
				c.OwnerId = 'XXXXXXXXXXXXXXXX';
				Task tsk = new Task();
				tsk.WhoId = c.Id;
				tsk.ActivityDate = System.today();
				tsk.Status = 'Completed';
				tsk.Subject = 'Ownership Transferred';

				taskList.add(tsk);

			} //close if statement
			else {
				c.OwnerId='XXXXXXXXXXXXXXXX';
				Task tsk = new Task();
				tsk.WhoId = c.Id;
				tsk.ActivityDate = System.today();
				tsk.Status = 'Completed';
				tsk.Subject = 'Ownership Transferred';

				taskList.add(tsk);
			} //close else
		} //close for-loop

		try {
			insert taskList;
		} catch (system.dmlexception e) {
			System.debug('Tasks not inserted: ' + e);
		}

		try {
			update scope;
		} catch (system.dmlexception e) {
			System.debug('Scope not updated: ' + e);
		}

	} //close execute method

	global void finish(Database.BatchableContext BC) {

		AsyncApexJob a = [Select Id, Status, NumberOfErrors, JobItemsProcessed,
      		TotalJobItems, CreatedBy.Email
      		from AsyncApexJob where Id =
      		:BC.getJobId()];

		// Create and send an email with the results of the batch.
		Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

		mail.setToAddresses(new String[] {a.CreatedBy.Email});
		mail.setReplyTo('batch@mycompany.com');
		mail.setSenderDisplayName('Batch Processing');
		mail.setSubject('Contact Update ' + a.Status);
		mail.setPlainTextBody('The batch apex job processed ' + a.TotalJobItems +
		' batches with ' + a.NumberofErrors + ' failures.');

		Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });

	} //close finish method
} //close class

Apex Class Used to Schedule the Batch Apex Class:

global class ScheduleUpdateContacts implements Schedulable {
	global void execute(SchedulableContext SC) {
		UpdateAllContacts uac = new UpdateAllContacts();
		database.executebatch(uac);
	} //close execute method
} //close class

Test Class that Achieves 94% Code Coverage:

@isTest
private class UpdateAllContactsTest {

    static testMethod void TestUpdateAllContacts() {

    	// User Id's for the two sales managers.
    	String aId = 'XXXXXXXXXXXXXXXX';
    	String bId = 'XXXXXXXXXXXXXXXX';

    	List <Contact> contacts = new List <Contact>();
    	List <Task> tasks = new List <Task>();

    	Test.StartTest();

    	// Create 50 Contacts and assign them to the sales manager of the opposite location.
    	for (integer i=0; i<50; i++) {
    		Contact c = new Contact(FirstName='Test',
    					LastName='Contact'+ i,
    					Location__c = 'Location 1',
    					OwnerId = bId);
    		contacts.add(c);

    	} //close for-loop

    	// Create 50 more Contacts and assign them to the sales manager of the opposite location.
    	for (integer i=0; i<50; i++) {
    		Contact c = new Contact(FirstName='Test',
    					LastName='Contact' + i + i,
    					Location__c = 'Location 2',
    					OwnerId = aId);
    		contacts.add(c);

    	} //close for-loop

    	insert contacts;

    	List <Contact> cont = [Select ID, FirstName from Contact Where FirstName=:'Test' limit 200];

    	// Create a Task for each Contact that was just inserted.  Set the date of the task so that it will set the
    	// Last Activity Date to a date older than your protection period.
    	for (Integer i=0; i<100; i++) {
    		Task tsk = new Task();
    		tsk.WhoId = cont.get(i).Id;
			tsk.ActivityDate = System.today() - 15;
			tsk.Status = 'Completed';
			tsk.Subject = 'Test Subject';

			tasks.add(tsk);

    	} // close for-loop

    	try {
    		insert tasks;
    	} catch (System.DMLexception e) {
    		System.debug('Task List not inserted: ' + e);
    	}

    	// Call the Batch Apex method.
    	UpdateAllContacts uac = new UpdateAllContacts();
    	ID batchprocessid = Database.executeBatch(uac);
		Test.StopTest();

		AsyncApexJob async = [Select Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems from AsyncApexJob where Id = :batchprocessid];
		System.debug('Final results are ' + async);

		System.AssertEquals(async.NumberOfErrors, 0);
		System.AssertEquals([Select count() from Contact Where OwnerId=:aId AND FirstName='Test'], 50);
		System.AssertEquals([Select count() from Contact Where OwnerId=:bId AND FirstName='Test'], 50);
		System.AssertEquals([Select count() from Task Where Subject = 'Test Subject'], 100);

    } //close testmethod

} //close Class

Once you’ve successfully saved your Apex Classes go to Setup –> Develop –> Apex Classes –> Schedule Apex. Use the class above that implements the Schedulable interface and select the frequency that you want the class to run.

Hope this is helpful. As always, I look forward to your thoughts and/or feedback.

Tags: , ,

A Business Use Case for Batch Apex

9 Responses

  1. Nice job Clint! I forwarded this on to a customer that is struggling with Batch Apex.

    Jeff Douglas July 23, 2010 at 11:17 am #
  2. Thanks Jeff! Glad this was helpful.

    Clint Lee July 23, 2010 at 11:48 am #
  3. Hey Clint,

    Is there a way to adjust this to reassign Leads based on ActivityHistory per Lead? In other words, I have a set of Activities that would keep a Lead under a Sales Rep’s ownership. If they have not performed any of these activities in the past 10 days then reassign?

    Josh October 12, 2010 at 1:29 am #
  4. Hi Josh,

    First off, you could change all instances of ‘Contact’ to ‘Lead’ in the code sample. However, this would just allow you to perform this same functionality but on Leads instead of Contacts.

    I believe what you’re referring to is more complex because it sounds like you’d need to filter your Leads based on some query of the ActivityHistory. For example, your query might be “Give me all Leads that have not had an Activity of Type = ‘Phone Call’ in the past 10 days.” Then, you’d reassign those Leads.

    A great scenario would be to have a Roll-Up Summary field on the Lead object that let you aggregate the number of total activities that fit your criteria. For example, you could summarize the number of Phone Calls and Emails within the past 10 days, and put that number into the Roll-Up summary field. Unfortunately, Salesforce doesn’t support Roll-Up summaries on Activities. There is an idea that you could vote for here that speaks to this.

    Clint Lee October 12, 2010 at 1:42 pm #
  5. Josh,

    After some further thought, you might be able to meet your requirements by tweaking the code. You’ll find links to an example and test class below.

    http://snipplr.com/view/42238/reassign-leads-in-salesforce–apex-language/
    http://snipplr.com/view/42241/test-class-for-lead-reassignment/

    Hope that helps,

    Clint

    Clint Lee October 13, 2010 at 7:42 pm #
  6. Hey! Clint,

    Very nice article, it is just right for someone to get started with Batch classes, your real world example helped a lot.

    Mitesh January 31, 2012 at 1:21 pm #
  7. Thanks, Mitesh! Glad it was helpful for you.

    Clint Lee January 31, 2012 at 1:33 pm #
  8. Hey Buddy!!!

    You know what this is what I looking for and you save me brother…

    Nice Work.

    Steve April 27, 2012 at 4:49 pm #
  9. Thanks, Steve. Glad you got something from it.

    Clint Lee April 28, 2012 at 3:09 pm #

Leave a Reply