Apex save class when scheduled > ‘This schedulable class has jobs pending or in progress’

How do I develop in a class that is referenced in a scheduled job?You will see the following error when you try to save this class:

This schedulable class has jobs pending or in progress

There is the workaround, what I did is create a Job scheduler so basically 1 scheduled class from where I reference all my scheduled classes. We are not initializing the class by creating a object of the class by name:

global class App_Job_Scheduler implements Schedulable {

	global void execute(SchedulableContext sc) {
		System.Type closedAccountsScheduler = Type.forName('Job_Closed_Accounts');
		Schedulable closedAccountObject = (Schedulable)closedAccountsScheduler.newInstance();
		closedAccountObject.execute(sc);
       }
}

Scheduler class, now you can schedule this class and will not get the >This schedulable class has jobs pending or in progress error when you try to save any code to App_Service or Selector_Financial_Account

global class Job_Closed_Accounts implements System.Schedulable, Database.Batchable<SObject>, Database.Stateful, Database.AllowsCallouts {

	global Database.QueryLocator start(Database.BatchableContext BC) {
		return App_Selector_Financial_Account.newInstance().selectAccountAndRelatedObjectsToDelete();
	}

    global void execute(Database.BatchableContext BC, List<Financial_Account__c> financialAccountIdsToBeDeleted) {
		List<Financial_Account__c> financialAccountsToDelete = App_Selector_Financial_Account.newInstance().selectAccountAndRelatedObjectsToDelete(financialAccountIdsToBeDeleted);
		App_Service.instance.deleteRelatedAccountsRecords(financialAccountsToDelete);
	}

	global void finish(Database.BatchableContext BC) {}
}

Generating an Email-to-Case thread id using Apex

How to generate Email-to-Case thread id inside template:

Generate the thread Id

public static String getThreadId(String caseId){
public static String getThreadId(String caseId){
  return '[ ref:_'
     + UserInfo.getOrganizationId().left(5)
     + UserInfo.getOrganizationId().mid(11,4) + '._'
     + caseId.left(5)
     + caseId.mid(10,5) + ':ref ]';
}

Send customEmail and Add ThreadId

public void sendCustomEmail(User selectedUser, Contact selectedContact, String templateName, Map<String, String> customVariables){
    EmailTemplate emailTemplate = [select Id, Subject, HtmlValue, Body from EmailTemplate where DeveloperName=:templateName];

    if (selectedContact==null){
       selectedContact =  selectedUser.Contact;
    }

    String customSubject = emailTemplate.Subject;

    if (customVariables.containsKey('joinContactName')){
      customSubject = customSubject.replace('{!JointContactName}', customVariables.get('joinContactName'));
    }
    //Add ThreadId to the Subject
    if (customVariables.containsKey('ThreadId')) {
      customSubject = customSubject.replace('{!ThreadId}', customVariables.get('ThreadId'));
    }

    String htmlBody = emailTemplate.HtmlValue;
    htmlBody = htmlBody.replace('{!Contact.FirstName}',selectedContact.FirstName);
    htmlBody = htmlBody.replace('{!Contact.Email}', selectedContact.Email);
    if (customVariables.containsKey('Link')){
      htmlBody = htmlBody.replace('{!CustomUrl}', customVariables.get('Link'));
    } else if (customVariables.containsKey('link')){
      htmlBody = htmlBody.replace('{!CustomUrl}', customVariables.get('link'));
    } else if (customVariables.containsKey('joinContactName')){
      htmlBody = htmlBody.replace('{!JointContactName}', customVariables.get('joinContactName'));
    }

    //Add ThreadId to the htmlBody
    if (customVariables.containsKey('ThreadId')){
      htmlBody = htmlBody.replace('{!ThreadId}', customVariables.get('ThreadId'));
    }

    String plainBody = emailTemplate.Body;
    plainBody = plainBody.replace('{!Contact.FirstName}',selectedContact.FirstName);
    plainBody = plainBody.replace('{!Contact.Email}', selectedContact.Email);
    if (customVariables.containsKey('Link')){
      plainBody = plainBody.replace('{!CustomUrl}', customVariables.get('Link'));
    } else if (customVariables.containsKey('link')){
      plainBody = plainBody.replace('{!CustomUrl}', customVariables.get('link'));
    } else if (customVariables.containsKey('joinContactName')){
      plainBody = plainBody.replace('{!JointContactName}', customVariables.get('joinContactName'));
    }

    //Add the threadId to the plainBody
    if (customVariables.containsKey('ThreadId')){
      plainBody = plainBody.replace('{!ThreadId}', customVariables.get('ThreadId'));
    }

    Messaging.Singleemailmessage email = new Messaging.Singleemailmessage();
    List<OrgWideEmailAddress> orgWideEmailAddress = [Select Id, Address from OrgWideEmailAddress where DisplayName=:ORGWIDEEMAILADDRESSNAME];
    //Set ReplyTo as the Email-to-Case email address
    if (customVariables.containsKey('ThreadId')){
      List<EmailServicesAddress> emailServicesAddress = [SELECT Id,AuthorizedSenders,EmailDomainName,IsActive,LocalPart FROM EmailServicesAddress where IsActive=true and (LocalPart like '%service%' or LocalPart like '%support%')];
      if (!emailServicesAddress.isEmpty()){
        EmailServicesAddress emailToCase = emailServicesAddress.get(0);
        email.setReplyTo(emailToCase.LocalPart +'@'+ emailToCase.EmailDomainName);
        email.setSenderDisplayName('Client Support');
      } else {
        email.setReplyTo('customer-service@gmail.com');
        email.setSenderDisplayName('Client Support');
      }
    }

    email.setTargetObjectId(selectedContact.Id);
    email.setSaveAsActivity(true);

    email.setSubject(customSubject);

    email.setHtmlBody(htmlBody);
    email.setPlainTextBody(plainBody);

    if (customVariables.containsKey('WhatId')){
      email.setWhatId(customVariables.get('WhatId'));
    }

    Messaging.sendEmail(new Messaging.SingleEmailmessage[] {email});
}

Putting it all together

Map<String, String> mappingOfCaseFields = new Map<String, String>();
mappingOfCaseFields.put('ThreadId', App_Service.getThreadId(paymentCaseId));
App_Service.instance.sendCustomEmail(null, contact, 'App_Support_Template', mappingOfCaseFields);

Apex callout PATCH to Heroku Controller workaround

Setup a method to send Aync call to Heroku, add ‘?_HttpMethod=PATCH’ to the path to notify Heroku it’s a patch request

@future(callout=true)
	public static void callApiEndpointAsync(String apiEndpoint, String method, String aPayload){
		HttpRequest req = new HttpRequest();
		List<String> theArgs = new List<String>();
		HttpResponse res = new HttpResponse();
    try {
      if (apiEndpoint != null) {
        req.setTimeout(120000);
				if ('PATCH'.equals(method)){
					apiEndpoint += '?_HttpMethod=PATCH';
					req.setMethod('POST');
				} else {
					req.setMethod(method);
				}

        setAuthHeaderAsync(req);
        req.setEndpoint(herokuUrl + apiEndpoint);
				if (aPayload!=null)
					req.setBody(String.valueOf(aPayload));

				if (Test.isRunningTest() && (mock!=null)) {
					 mock.respond(req);
			 	} else {
					Integer retry = 0;

					while (retry < 2){
						Http http = new Http();
		        res =  http.send(req);
						if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
							retry +=2;
						} else if (res.getStatusCode()==503 || res.getStatusCode()==400){
							retry +=1;
							res = http.send(req);
						} else {
							retry +=1;
							theArgs.add('Api');
							theArgs.add(req.getEndpoint());
							theArgs.add(res.getBody());
						}
					}
					throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', null, theArgs));
				}

      } else {
        System.debug('Service apiEndpoint and payload must not be null');
        throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED'));
      }
    } catch (Exception ex) {
			theArgs.add('Api');
			theArgs.add(req.getEndpoint());
			theArgs.add(res.getBody());
			throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', ex, theArgs));
			
    }
}

Spring controller checks the RequestParam _HttpMethod to see if it’s a POST or PATCH request

@RequestMapping(value = "/customer", method = RequestMethod.POST, produces = "application/json")
@ResponseBody
@ApiOperation(value = "Creates new payment customer")
@ApiResponses(value = {@ApiResponse(code = 200, message = "Creates new payment customer")})
public String createCustomer(@RequestBody PaymentCustomerWrapper paymentCustomerWrapper, @RequestParam(value="_HttpMethod", defaultValue="POST")  String httpMethod)
        throws IOException, URISyntaxException {
     if (httpMethod!=null && "PATCH".equals(httpMethod)){
         itsLogger.debug("update payment customer {}", paymentCustomerWrapper.toString());
         return paymentService.updatePaymentCustomer(paymentCustomerWrapper).toJson();
    } else {
         itsLogger.debug("create payment customer {}", paymentCustomerWrapper.toString());
         return paymentService.createChargeBeeCustomer(paymentCustomerWrapper).toJson();
    }
}

Heroku deploy local jar to Maven Dependencies

Sometime a jar is not available via Maven Central Repository and you need to load a jar from your local filesystem. Copy the jar file to a lib directory in your project.

Execute the following command via command line

mvn deploy:deploy-file -Durl=file:salesforce/lib/ 
-Dfile=salesforce/lib/emp-connector-0.0.1-SNAPSHOT-phat.jar 
-DgroupId=com.salesforce.conduit 
-Dpackaging=jar 
-Dversion=0.0.1-SNAPSHOT 
-DartifactId=emp-connector

Add the following to maven pom.xml

 <repositories>
    <repository>
        <id>project.local</id>
        <name>project</name>
        <url>file:${project.basedir}/lib</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
 </repositories>
 <dependencies>
    <dependency>
        <groupId>com.salesforce.conduit</groupId>
        <artifactId>emp-connector</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

Apex Return Diff fields between records

Sometimes you want to determine what has changed between an existing record in the database and an update to the record. The diffRecord method will return all the fields that changed for a specific record.

Compare the difference between to records and return changed fields

public SObject diffRecord(SObject updatedRecord, SObject currentRecord, String sObjectName){
  SObjectType objToken = Schema.getGlobalDescribe().get(sObjectName);
  DescribeSObjectResult objDef = objToken.getDescribe();
  Map<String, SObjectField> fields = objDef.fields.getMap();
  Type classType = Type.forName(sObjectName);
  SObject diffRecord = (SObject)JSON.deserialize('{}', classType);
  for (String field : fields.keySet()){
    if (!fieldsToExclude.contains(fields.get(field).getDescribe().getName()) && (fields.get(field).getDescribe().isUpdateable() || fields.get(field).getDescribe().isCreateable())){
        if (updatedRecord.get(field)!=currentRecord.get(field))
        	diffRecord.put(field, updatedRecord.get(field));
    }
  }
  return diffRecord;
}

Test that only changed fields are returned

@isTest static void testDiffRecords(){
  String currentRecordJSON = '{"Id":"a0L63000000qc43EAA","Account_Type__c":"Managed","Amount__c":50.00,"Category__c":"Deposit","Description__c":"Deposit","End_Date__c":"2020-03-31T15:06:57.000+0000","Frequency__c":"Monthly"}';
  String updatedRecordJSON = '{"Amount__c":50.0,"Id":"a0L63000000qc43EAA","Frequency__c":"Annually"}';
  Type classType = Type.forName('Account');
  SObject currentRecord = (SObject)JSON.deserialize(currentRecordJSON, classType);
  SObject updatedRecord = (SObject)JSON.deserialize(updatedRecordJSON, classType);
  Test.startTest();
    SObject diffRecord = App_Service.instance.diffRecord(updatedRecord, currentRecord, 'Account');
    System.assertEquals(diffRecord.get('Amount__c'), null);
    System.assertEquals(diffRecord.get('Frequency__c'), 'Annually');
    System.assertEquals(diffRecord.get('Account_Type__c'), null);
    System.assertEquals(diffRecord.get('Category__c'), null);
  Test.stopTest();
}

Apex Compare two Records and Merge updated fields

When you have two record, one being the existing queried record and the other the updated record. To merge the original with the updated field without committing the changes you can iterate the current record’s fields and check which of the fields changed. Update the changed fields to the new updated fields value. Now we have an merged record. Here is how I went about doing it.

Original SObject record

{"Account_Type__c":"Managed","Amount__c":50.00,"Category__c":"Deposit",
"Description__c":"Deposit","End_Date__c":"2020-03-31T15:06:57.000+0000",
"Frequency__c":"Monthly"}

Update SObject record

{"Amount__c":500.0,"Id":"a0L63000000qc43EAA","Frequency__c":"Annually"}

Merge two records with changes

private static Set<String> fieldsToExclude = new Set<String>{'OwnerId','OtherGeocodeAccuracy','MailingGeocodeAccuracy','BillingGeocodeAccuracy','ShippingGeocodeAccuracy'};
public SObject mergeRecords(SObject updatedRecord, SObject currentRecord, String sObjectName){
  SObjectType objToken = Schema.getGlobalDescribe().get(sObjectName);
  DescribeSObjectResult objDef = objToken.getDescribe();
  Map<String, SObjectField> fields = objDef.fields.getMap();
  Type classType = Type.forName(sObjectName);
  SObject mergedRecord = (SObject)JSON.deserialize('{}', classType);
  for (String field : fields.keySet()){
    if (!fieldsToExclude.contains(fields.get(field).getDescribe().getName()) && (fields.get(field).getDescribe().isUpdateable() || fields.get(field).getDescribe().isCreateable())){
        if (updatedRecord.get(field)!=null){
        	mergedRecord.put(field, updatedRecord.get(field));
    	} else if (currentRecord.get(field)!=null){
        	mergedRecord.put(field, currentRecord.get(field));
    	}
    }
  }
  return mergedRecord;
}

Test that fields were merged correctly

@isTest static void testMergeRecords(){
  String currentRecordJSON = '{"Id":"a0L63000000qc43EAA","Account_Type__c":"Managed","Amount__c":50.00,"Category__c":"Deposit","Description__c":"Deposit","End_Date__c":"2020-03-31T15:06:57.000+0000","Frequency__c":"Monthly"}';
  String updatedRecordJSON = '{"Amount__c":500.0,"Id":"a0L63000000qc43EAA","Frequency__c":"Annually"}';
  Type classType = Type.forName('Account');
  SObject currentRecord = (SObject)JSON.deserialize(currentRecordJSON, classType);
  SObject updatedRecord = (SObject)JSON.deserialize(updatedRecordJSON, classType);
  Test.startTest();
    SObject mergedRecord = mergeRecords(updatedRecord, currentRecord, 'Account');
    System.assertEquals(mergedRecord.get('Amount__c'), 500.0);
    System.assertEquals(mergedRecord.get('Frequency__c'), 'Annually');
    System.assertEquals(mergedRecord.get('Account_Type__c'), 'Managed');
    System.assertEquals(mergedRecord.get('Category__c'), 'Deposit');
  Test.stopTest();
}

Apex fflib_SObjectUnitOfWork SimpleDML UNABLE_TO_LOCK_ROW nested try catch

UNABLE_TO_LOCK_ROW issue is very common if you have multiple users updating the record at the same time .Or say a batch job is running and is updating a record and same record another trigger or code snippet (usually a future method) is updating. A way to solve this is to retry multiple times to see if the record has been unlocked for update.

Multi-try will try update 3 times before throwing and error

public class SimpleDML implements IDML
{
	public void dmlUpdate(List<SObject> objList){
		try {
			update objList;
		} catch (System.DMLException ex1){
			if (StatusCode.UNABLE_TO_LOCK_ROW==ex1.getDmlType(0)){
				try{
					update objList;
				} catch (System.DMLException ex2) {
					if (StatusCode.UNABLE_TO_LOCK_ROW==ex2.getDmlType(0)){
						update objList;
					} else {
						throw ex2;
					}
				}
			} else {
				throw ex1;
			}
		}
	}
}

Apex Caching Performance Impact

Apex Caching is great for saving some basic Id’s of a user like his Account, Contact Id’s after he logs in so you don’t have to query for them. Reduces the amount of queries and improves speed. Caching is good for saving large blobs that don’t change frequently.

Generic Cache Service to put, retrieve and remove cached item

public with sharing class App_Service_Cache {

		private Boolean cacheEnabled;

		public App_Service_Cache() {
			  cacheEnabled = true;
		}

    public Boolean toggleEnabled() { // Use for testing misses
        cacheEnabled = !cacheEnabled;
        return cacheEnabled;
    }

    public Object get(String key) {
        if (!cacheEnabled) return null;
        Object value = Cache.Session.get(key);
        if (value != null) System.debug(LoggingLevel.DEBUG, 'Hit for key ' + key);
        return value;
    }

    public void put(String key, Object value, Integer ttl) {
        if (!cacheEnabled) return;
        Cache.Session.put(key, value, ttl);
        // for redundancy, save to DB
        System.debug(LoggingLevel.DEBUG, 'put() for key ' + key);
    }

    public Boolean remove(String key) {
        if (!cacheEnabled) return false;
        Boolean removed = Cache.Session.remove(key);
        if (removed) {
            System.debug(LoggingLevel.DEBUG, 'Removed key ' + key);
            return true;
        } else return false;
    }
}

Test cache get is faster than query

@isTest
private class App_Service_Cache_Test {

	@isTest
	static void testCachePerformance() {
		App_Service_Cache appCache = new App_Service_Cache();
		Map<String, String> communityUser = App_Global_Test.setupCommunityUserReturnIds();
		Id userId = communityUser.get('UserId');
		long startTime = System.currentTimeMillis();
		List<Contact> currentContact = [select Id, AccountId from Contact where Id IN (Select ContactId from User where Id=:userId)];
		long elapsedTime = System.currentTimeMillis() - startTime;
		appCache.put('userId', currentContact, 2000);
		System.debug(elapsedTime);

		long startCacheTime = System.currentTimeMillis();
		appCache.get('userId');
		long elapsedCacheTime = System.currentTimeMillis() - startCacheTime;
		System.assert(elapsedCacheTime < elapsedTime);
	}
}

Create a Salesforce EventBus

Create a new Planform Event Object and give it a name App Metadata (App_Metadata__e) which will also be the name of the topic. Create some fields you that you would like to listen to. In Apex add the following event

Build event payload

  List<App_Metadata.BPUpdateAccounts> updatedAccountList = new List<App_Metadata.BPUpdateAccounts>();
  App_Metadata.BPUpdateAccounts updatedAccount = new App_Metadata.BPUpdateAccounts();
  updatedAccount.setId(financialAccount.Id);
  updatedAccount.add(updatedAccount);
  new App_Rest_Events(new App_Rest_Events.Event(UserInfo.getUserId(), App_Rest_Events.EventType.UPDATE_ACCOUNT, 
  updatedAccountList)).publishToEventBus();

Publish Event to eventBus

  public void publishToEventBus(){
    Object eventPayload = buildEventPayload();
    EventBus.publish(new App_Metadata__e(Metadata_Json__c=JSON.serialize(eventPayload)));
  }

Now let’s create our event listen to subscript to the topic and consume the messages from the EventBus.

Add the emp-connector jar to project

<repositories>
        <repository>
            <id>emp-connector</id>
            <url>${project.basedir}/lib/emp-connector-0.0.1-SNAPSHOT-phat.jar</url>
        </repository>
    </repositories>

....

<dependency>
     <groupId>com.salesforce.conduit</groupId>
     <artifactId>emp-connector</artifactId>
     <version>0.0.1-SNAPSHOT</version>
</dependency>

Create TopicSubscription to your event table by name

package com.app.salesforce.eventbus;

import com.salesforce.emp.connector.BayeuxParameters;
import com.salesforce.emp.connector.EmpConnector;
import com.salesforce.emp.connector.TopicSubscription;

import java.net.URL;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static com.salesforce.emp.connector.LoginHelper.login;

/**
 * Created by thysmichels on 5/15/17.
 */
public class SalesforceEventBus {

    private static final String SALESFORCE_USERNAME="salesforceUsername";
    private static final String SALESFORCE_PASSWORD="salesforcePassword";

    public static void main(String[] argv) throws Exception {

        long replayFrom = EmpConnector.REPLAY_FROM_EARLIEST;

        BayeuxParameters params;
        BayeuxParameters custom = getBayeuxParamWithSpecifiedAPIVersion("39.0");
        try {
            params = login(new URL("https://test.salesforce.com"),SALESFORCE_USERNAME, SALESFORCE_PASSWORD, custom);

        } catch (Exception e) {
            e.printStackTrace(System.err);
            System.exit(1);
            throw e;
        }

        Consumer<Map<String, Object>> consumer = event -> System.out.println(String.format("Received:\n%s", event));
        EmpConnector connector = new EmpConnector(params);

        connector.start().get(5, TimeUnit.SECONDS);

        TopicSubscription subscription = connector.subscribe("/event/App_Metadata__e", replayFrom, consumer).get(5, TimeUnit.SECONDS);

        System.out.println(String.format("Subscribed: %s", subscription));
    }


    private static BayeuxParameters getBayeuxParamWithSpecifiedAPIVersion(String apiVersion) {
        BayeuxParameters params = new BayeuxParameters() {

            @Override
            public String version() {
                return apiVersion;
            }

            @Override
            public String bearerToken() {
                return null;
            }

        };
        return  params;
    }
}

Topic receives response from subscribed channel

Subscribed: Subscription [/event/App_Metadata__e:-2]
Received:
{schema=D6-eSgLDrahnNjuNI52XAg, payload={CreatedById=00521000000UcF0, CreatedDate=2017-05-15T18:35:10Z, 
Metadata_Json__c="{\"accounts\":[{\"id\":\"28706236\",\"providerAccountId\":14736,\"sfId\":\"a0721000001A6fOAAS\"},
{\"id\":\"YL1111\",\"providerAccountId\":1466,\"sfId\":\"a0721000001A6fPAAS\"}],\"event\":\"UPDATE_GOAL\",\"linkAccounts\":null,
\"financialAccountMap\":null,\"goals\":[{\"id\":\"a0821000000OVPUAA4\",\"linkedAccounts\":[\"a0721000001ADqkAAG\"],
\"status\":\"In Progress\"},{\"id\":\"a0821000000OVPZAA4\",\"linkedAccounts\":null,\"status\":\"In Progress\"},{\"id\":\"a0821000000OVPeAAO\",\"linkedAccounts\":[\"a0721000001ADqaAAG\"],\"status\":\"In Progress\"}],
\"linkUserContext\":{\"accountList\":null,\"additionalProperties\":null,\"deleteAccountList\":null,\"email\":\"blah@email.com\",
\"password\":\"p@assw0rd\",
\"providerAccountIds\":null,\"sfClientId\":\"0033600000NO1QjAAL\",\"sfHouseholdId\":\"0013600000VNXflAAH\",\"sfUserId\":\"00521000000UcF0AAK\",
\"type\":\"YL\",\"username\":\"YL00F0A\"},\"priority\":5,\"providerAccounts\":null,
\"updatedAccounts\":null,\"updatedGoals\":[{\"id\":\"a0821000000OVPZAA4\",\"linkedAccounts\":null}],\"mapKeys\":{\"event\":\"event\",\"priority\":\"priority\",\"linkUserContext\":\"linkUserContext\",\"goals\":\"goals\",
\"accounts\":\"accounts\",\"providerAccounts\":\"providerAccounts\",\"updatedAccounts\":\"updatedAccounts\",\"updatedGoals\":
\"updatedGoals\",\"goalFinancialAccountMap\":\"goalFinancialAccountMap\",\"fastlinkAccounts\":\"fastlinkAccounts\"},
\"mapKeysFlag\":true,\"serializeNulls\":false}"}, event={replayId=1}}
%d bloggers like this: