Sunday, 24 February 2019

Trigger Design Pattern in Apex


Trigger Design Patterns Without Framework :
=================================

Frameworks improve reliability and maintainability,and make probelms easier to debug.

ex :
1.Follow up on lead account(Process Builder)
a. Insert lead with 'Open not Contacted' with good email
b. Create a follow up task.

2.taskTriggerSetStatus
a.Task created for lead
b.if the lead has a task, set its status to 'Working-Contacted'
or 'Working - harder '.

3.leadTriggerSetFollowup
a.if a lead has a working status
b.Create a follow-up task (reminder).

4.taskTriggerTrackCount
a.Task is created,updated or deleted
b.Update # of tasks on the lead

5.First Owner Worked (Process Builder)
a.Lead status starts with Working and Owner is a user.
b.Set first owner worked field.

Note :
=========
1.controling the trigger order
2.Combining DML operations
3.controlling reentrancy


Dynamic Trigger Handler :
===========================

public interface ITriggerExtension
{

  void HandlerTrigger(TriggerOperation operationType,
              List<SObject> newList,List<SObject> oldList,
              Map<ID,SObject> newMap,Map<ID,SObject> oldMap);
}


public with sharing class dynamicTaskTriggerHandler implements ITriggerExtension{

public void HandleTrigger(TriggerOperation operationType,List<SObject> newList,List<SObject> oldList,
                         Map<ID,SObject> newMap,Map<ID,SObject> oldMap)
 {
   system.debug('Trigger execution was called');
 }

}


trigger taskTrigger on Task (before insert,after insert,before update,after update,before delete,after delete){

simpleLeadUpdater leadUpdater = new simpleLeadUpdater();

// What happens when you change the order of these two triggers handlers?

  if(trigger.operationType == TriggerOperation.AFTER_INSERT)
    {
      taskSetStatus.handleTrigger(trigger.new,leadUpdater);
    }

  if(trigger.operationType == TriggerOperation.AFTER_INSERT ||
     trigger.operationType == TriggerOperation.AFTER_UPDATE ||
     trigger.operationType == TriggerOperation.AFTER_DELETE )
     {
     
       taskTrackCount.handleTrigger (trigger.operationType,trigger.new,trigger.old,leadUpdater);
    }

 List<ITriggerExtension> dynamicTriggers = TriggerExtensionSupport.getTriggerClasses('Task');
  for(ITriggerExtension trig : dynamic Triggers)
   {
      trig.HandleTrigger(trigger.operationType,trigger.new,trigger.old,trigger.newMap,trigger.oldMap);
 
   }

  leadUpdater.updateLeads();
}

public with sharing class TriggerExtensionSupport{

public static List<ITriggerExtension> getTriggerClasses(String objectType)
 {
    List<Dynamic_Trigger_mdt> triggerSettings = [ Select Class_Name__c from Dynamic_Trigger__mdt where
                                                  Object_Type__c = : objectType OrderBy Priority__c ASC];
    List<ITriggerExtension> results = new List<ITriggerExtension>();

    for(Dynamic_Trigger__mdt setting : triggerSettings)
    {
      System.Type thetype = Type.forName(setting.Class_Name__c);
       if(thetype == null)
        thetype = Type.forName('',setting.Class_Name__c); // Try resolving local class
       if(thetype!=null){
           Object theobject = thetype.newInstance();
           if(theobject instanceof ITriggerExtension) results.add ((ITriggerExtension) theobject);
        }
    }

   return results ;
 }

}

public with sharing class taskTrackCount {

public static void handleTrigger (TriggerOperation operationType, List<Task> newList,List<Task> oldList,simpleLeadUpdater leadUpdater)
  {
     Set<ID> leadIds = new Set<ID>();
   
     if(operationType == TriggerOperation.AFTER_UPDATE || operationType == TriggerOperation.AFTER_INSERT)
       {
          for(Task t : newList)
           {
             if(t.WhoId!=null && t.WhoId.getSObjectType()==Schema.Lead.SObjectType)
              leadIds.add(t.WhoId);
           }
       }
 
     if(operationType == TriggerOperation.AFTER_UPDATE || operationType == TriggerOperation.AFTER_DELETE)
         {
            for(Task t : oldList)
             if(t.whoId !=null && t.whoID.getSObjectType() == Schema.Lead.SObjectType)
               leadIds.add(t.whoId);
         }

     List<Lead> leads = [Select ID,Task_Count__c from Lead where ID in : leadIds];
     List<AggregateResult> tasks = [Select Count(ID) items,WhoId from Task where WhoId in : leadIds group by WhoID];
     Map<ID,Interger> taskCounts = new Map<ID,Interger>();
 
     for(AggregateResult ar : tasks)
     {
       taskCounts.put((ID)ar.get('WhoId'),(Interger) ar.get('items));
     }
   
     for(Lead ld : leads)
      {
        if(ld.Task_Count__c != taskCounts.get(ld.Id))
          {
            Lead toUpdate = leadUpdater.getLead(ld.id);
            toUpdate.Task_Count__c = taskCounts.get(ld.id);
          }
      }
   }
}

public with sharing class taskSetStatus {

private static Set<ID> statusUpdated = new Set<ID>();

public static void handleTrigger (List<Task> newTasks,simpleLeadUpdater leadUpdater){

    Set<ID> leadIds = new Set<ID>();

     for(Task t : newTasks)
      {
        if(t.whoId!=null && t.whoId.getSObjectType() == Schema.Lead.SObjectType && !t.Declarative_Created__c)
            leadIds.add(t.whoId);
      }

    List<Lead> leads = [Slect ID,Status from Lead where ID in : leadids];

   for(Lead ld : leads)
    {
       if(statusUpdated.contains(ld.id)) continue; // Skip those already updated

       switch on ld.status
        {
           when 'Open - Not Contacted'
            {
              Lead toUpdate = leadUpdater.getLead(ld.id);
               toUpdate.status = 'Working - Contacted ';
               statusUpdated.add(ld.id);
            }
           when 'Working - Contacted'
            {
              Lead toUpdate = leadUpdater.getLead(ld.id);
               toUpdate.status = 'Working Harder ';
                statusUpdated.add(ld.id);
            }
        }
    }
 }

}

public with sharing class simpleLeadUpdater {
  Map<ID,Lead> leadsToUpdate = new Map<ID, Lead>();

 public Lead getLead(ID leadID)
  {
    Lead targetLead = leadsToUpdate.get(leadID);
     if(targetLead == null)
      {
        targetLead = new Lead (ID = leadID);
         leadsToUpdate.put(leadID,targetLead);
      }
    return tragetLead;
  }

public void updateLeads()
  {
    if(leadsToUpdate.size()>0)
      update leadsToUpdate.values();
  }

}

Test Class :
===============
@istest
public with sharing class TestLeadActions{

@istest
public static void TestInsertSingleLead(){
    Test.startTest();
    InsertLeads(1,'Open-Not Contacted');
    Test.stopTest();
  }

@isTest
public static void TestInsertBulkLead()
 {
   Long startTime = DateTime.now().getTime();
   Long startCPU = Limits.getCpuTime();
   Test.startTest();
   InsertLeads(200,'Open - Not Contacted');
   Test.stopTest();
   system.debug ('Elapsed : ' + (Datetime.Now().getTime() - startTime));
   system.debug ('Elapsed CPU : ' + (Limits.getCpuTime() - startCPU));

 }

public static void InsertLeads(Interger count,String leadstatus)
{
  List<Lead> leadsToInsert = new List<Lead>();
  for(Interger x=0;x<count;x++)
   {
      String xs = string.valueOf(x);
      leadsToInsert.add(new Lead(FirstName = 'f' +xs,LastName = 'l'+xs,
                         status=leadstatus,
                         Company = 'c' + xs,
                         email = 'e' + 'xs' + '@pluralsight.com',
                         Bypass_Declarative__c = 0 ));

   }
   insert leadsToInsert;
}

 Public static void UpdateLeadStatus()
  {
    List<Lead> leads = [ select Id, status from Lead ];
     for(Lead ld : leads)
       ld.status = 'Working - Contacted ';
     Test.startTest();
      update leads ;
    Test.stopTest();

  }
}

Sunday, 3 February 2019

Asynchronous Apex


Asynchronous Apex :
===================
Asynchronous Apex fires on Salesforce's queue and outside the context of triggers, which allows for massive workloads, expensive computations,and parallel processing.

Note : Asynchronous processes run on their own time in their own run context.

Across run contexts : NO guarantees on order of execution

On the Salesforce platform, we will sometimes need Asynchronous logic to bypass governor limits,manipulate certain objects when certain conditions occur,contact web services,or perform
high-volume workloads.

1.future methods
2.queueable jobs
3.scheduled apex
4.batch apex

Note : Queueable jobs offer monitoring, chaining and non-primitive data types.

Async Benefits :
==================
Asynchronous Processing or Async processing allows running of parallel processes.

Using Future methods for Higher Limits or Mixed DML :
========================================

Future method runs in the background.you can call a future method for executing long-running operations,such as callouts to external web services or any operation you'd like to run in its own thread, on its own time.

Future methods can be used for :
a. Taking advantage of higher governor limits.
b. Long running web service callouts
c. Mixed DML operations.

Note : you might often use future methods for mixed DML operations in unit tests.


you can add on the "@future" annotation to any function that uses primitive data types.

ex :

@future
public static void updateUserManager(Set<Id> userIds,Id managerId)
{
  // update all users in specified IDs to have new specified Manager ID
    List<User> userToUpdate = new List<User>();
   for(Id userId : userIds)
     {
       usersToUpdate.add(new User(Id=userId,Manager = managerId));
     }

  Database.SaveResult[] updateResults= Database.update(usersToUpdate);
}


ex :

public without sharing class AccountUtils
{
  public static void sortOwners(List<Account> oldAccounts,List<Account> newAccounts)
   {
      Map<Id,List<Account>> userIdToAccounts = getUserToAccounts(newAccounts);
       // For each user,calculate Account Revenue and decide if
       // performance tiers should be updated
      Map<Id,Decimal> userIdToRevenue = getUserRevenue(userIdToAccounts);
      // calls the future method to make AnnualRevenue updated on the user
      if(userIdToAccounts.keySet.size()>0)
       UserUtils.updateAnnualRevenue(userIdRevenue);
   }

  public static Map<Id,List<Account>> getUserToAccounts(List<Account> accountsByUser)
   {
      Map<Id,List<Account>> userIdToAccounts=new Map<Id,List<Account>>();
     
       for(Account account : accountsByUsers)
         {
             if(account.AnnualRevenue !=null && account.AnnualRevenue !=0)
               {
                 if(userIdToAccounts.containskey(account.ownerId))
                     userIdToAccounts.get(account.Id).add(account);
                     else
                     userIdToAccounts.put(account.OwnerId,new List<Account>{account});
             
                }

         }
     return userIdToAccounts;
   }
 
 public static Map<Id,Decimal> getUserRevenue(Map<Id,List<Account>> userIdToAccounts)
  {
    Map<Id,Decimal> userIdToRevenue = new Map<Id, Decimal>();
     for(Id userId : userIdToAccounts.keySet())
      {
        // Iterate through Accounts to retrieve total
        Decimal totalRevenue =0;
         for(Account account : userIdToAccounts.get(userId))
          {
            totalRevenue += account.AnnualRevenue;
          }
         userIdToRevenue.put(userId,totalRevenue);

      }
     return userIdToRevenue;
  }

}


public without sharing class UserUtils
{

  public static Boolean testingException = false;

  public static final Map<String,Decimal> performanceTiers
    = new Map<String,Decimal>{'Low' => 0,'Medium' => 50000,'High' => 10000 };

  @future
  Public static void updateAnnualRevenue(Map<Id,Decimal> userIdsToAnnualRevenue)
  {
    // Retrieve users based on key values of the map : userIDsToAnnualRevenue
   List<User> users = getUsers(userIDsToAnnualRevenue.keySet());
   // Iterate through Users to determine if the performance tier
   // should be updated
   for(User user : users)
   {
     // Update Annual Revenue
      user.Annual_Revenue__c = userIDsToAnnualRevenue.get(user.Id);
       if(user.Annual_Revenue__c < performanceTiers.get('Medium'))
           user.Performance_Tier__c ='Low'; 
       if(user.Annual_Revenue__c >= performanceTiers.get('Medium')
          && user.Annual_Revenue__c < performanceTiers.get('High'))
           user.Performance_Tier__c='Medium';
       if(user.Annual_Revenue__c >= performanceTiers.get(High))
          user.performance_Tier__c='High';
    }

    update users;
  }

  public static List<User> getUsers(Set<Id> userIds)
  {
     List<User> users=[select Id,Name,Annual_Revenue__c,performance_Tier__c
                        from User where Id in : userIds];
      return users;

   }

}


Note :

Future methods can be quickly and easily built with benefits.

Drawbacks  of Future methods:
=============================
1.we can use only Primitive data types
2.No chaining
3.No monitoring

Testing future methods using Test.startTest() and Test.stopTest().

@isTest
private class AccountTest
{
  @testsetup
   static void setupData()
   {
     // Create multiple sales person users
      List<User> salesUsersToInsert = new List<User>();
     
      Profile adminProfile=[select Id, Name from Profile where Name ='System Administrator' LIMIT 1];
     // Low ,medium and high tier sales users
      User lowTierUser = new User(
           Performance_Tier__c='Low',
           FirstName ='Low',
           LastName ='User',
           Username='lowtieruser@example.com',
           Email ='lowtieruser@example.com',
           CompanyName ='Globomantics',
           Alias ='alias',
           Title ='Title',
           TimeZoneSidkey ='America/Chicago',
           EmailEncodingKey ='UTF-8',
           LanguageLocalKey='en_US',
           LocaleSidKey ='en_US',
           ProfileId=adminProfile.Id);
           
           User mediumTierUser = new User(
           Performance_Tier__c='Medium',
           FirstName ='Medium',
           LastName ='User',
           Username='mediumtieruser@example.com',
           Email ='mediumtieruser@example.com',
           CompanyName ='Globomantics',
           Alias ='alias',
           Title ='Title',
           TimeZoneSidkey ='America/Chicago',
           EmailEncodingKey ='UTF-8',
           LanguageLocalKey='en_US',
           LocaleSidKey ='en_US',
           ProfileId=adminProfile.Id);

           User highTierUser = new User(
           Performance_Tier__c='High',
           FirstName ='High',
           LastName ='User',
           Username='hightieruser@example.com',
           Email ='hightieruser@example.com',
           CompanyName ='Globomantics',
           Alias ='alias',
           Title ='Title',
           TimeZoneSidkey ='America/Chicago',
           EmailEncodingKey ='UTF-8',
           LanguageLocalKey='en_US',
           LocaleSidKey ='en_US',
           ProfileId=adminProfile.Id);

           // Add user records to list of users to insert
           salesUserToInsert.add(lowTierUser);
           salesUserToInsert.add(mediumTierUser);
           salesUserToInsert.add(highTierUser);
         
           insert salesUserToInsert;
       
           // Create test Accounts

            List<Account> accountsToInsert = new List<Account>();
             Account lowAccount = new Account (
                  Name = 'Low Account',
                  AnnualRevenue = 20000,
                  OwnerId= lowTierUser.Id);

             Account mediumAccount = new Account (
                  Name = 'Medium Account',
                  AnnualRevenue = 75000,
                  OwnerId= mediumTierUser.Id);

             Account highAccount = new Account (
                  Name = 'High Account',
                  AnnualRevenue = 100000,
                  OwnerId= highTierUser.Id);
         
             accountsToInsert.add(lowAccount);
             accountsToInsert.add(mediumAccount);
             accountsToInsert.add(highAccount);
           
             insert accountsToInsert;
 
   }

  @isTest
   static void sortOwnersSuccess()
   {
    // Retrieve users
    List<User> users = [ select Id, Performance_Tier__c from User
                          where LastName ='user'];
    // Retrieve Accounts
    List<Account> accounts = [select Id,Name,OwnerId,
                              AnnualRevenue from Account];
 
    // When
     List<Account> accountsToUpdate = new List<Account>();
     Test.startTest();
      Decimal updatedRevenue = UserUtils.performanceTiers.get('High') + 50000;
       Account accountUpdate = new Account(
            Id = accounts[0].Id,
            AnnualRevenue = updatedRevenue);
           
        accountsToUpdate.add(accountUpdate);
        update accountsToUpdate;
     Test.stopStart();

    // Then
   
    List<User> usersAfterUpdate = [ select Id,Performance_Tier__c
                                  from User where LastName = 'User' ];

   Interger numberofHighTierUsers =0;
 
    for ( User user : usersAfterUpdate)
      {
         if(user.Performance_Tier__c == 'High')
            numberofHighTierUsers + =1;
      }
   
    System.assertEquals (2,numberOfHighTierUsers, ' 2 High Tier Users Found After Update');


   }


}

Note : Do not make the mistake of a trigger that calls a Future Method that calls another trigger that calls a Future method and so on.


Monitoring ,Chaining processes with Queueable jobs :
=====================================

1.Beyond the Future Method

2.Chaining , monitoring , and non-primitive data types.

3. One Queueable job can call another queueable job , or future calls.
4. An Id means being able to retrieve data about other Queueable jobs.
5. Sobjects and declared objects can be passed in as input.

Note :
a. Possibility of chained parallel processes
b. Chanined parallelism means higher workloads.


Queueable Jobs :
=================
Queueable Jobs are derived from its interface, and as such : each Queueable class must have an execute method.

ex :

public class QueueableProcess implements Queueable
{
   // Every Queueable job has an 'execute' method
  public void execute (QueueableContext context)
  {
      doProcessing();
  }

}


To enqueue the job using System.enqueueJob.

System.enqueueJob(new QueueableProcess());

As an argument, we would pass an instance  of the queueable class itself as specifying which job to enqueue.

public without sharing class OpportunityUtils
{
  public static Map<Interger, List<sObject>> processProducts (List<Opportunity> oldOpps,List<Opportunity> newOpps)
   {
     // Collect all Opportunities moved to 'Closed Won'
     Map<Id,Opportunity> accountIdToOpportunity = new Map<Id,Opportunity>();
     Map<Id ,Opportunity> leadIdToOportunity = new Map<Id,Opportunity>();
     Map<Id,Opportunity> opportunityIdToOpportunity=new Map<Id,Opportunity>();
 
    for(Interger i=0; i< newOpps.size(); i++)
     {
       Opportunity opportunity = newOpps[i];
       Opportunity oldOpportunity = oldOpps == null ? null : oldOpps[i];
     
        if(oldOpportunity != null)

           if(opportunity.StageName != oldOpportunity.SatgeName && opportunity.SatgeName == 'Closed won')
            {
              accountIdToOpportunity.put(opportunity.AccountId,opportunity);
              leadIdToOpportunity.put(opportunity.Lead__c,opportunity);
              opportunityIdToOpportunity.put(opportunity.Id,opportunity);

            }
     }
 
   if(opportunityIdToOpportunity.keyset().size()==0)
    return new Map<Interger,List<sObject>>();
    // Retrieve object lists
 
    List<Account> accounts = getAccounts(accountIdToOpportunity.keySet());
 
    List<Case> cases = getCases(opportunityIdToOpportunity.keySet());
 
    List<Lead> leads = getLeads(leadIdToOpportunity.keySet());
   
    // Iterate through each object and make appropriate updates
    // For each Account,update its AnnualRevenue with the Amount
    // of the Opportunity record. For each Case, close it. For each
    // Lead,mark as 'Inactive'.

     for(Account account : accounts)
        account.AnnualRevenue = account.AnnualRevenue +
        accountIdToOpportunity.get(account.Id).Amount;

     for(Case caseRecord : cases)
       caseRecord.Status ='Closed';

     for(Lead lead : leads)
           lead.Status='Inactive';

   // Accounts can be updated synchronously
   
     update accounts;
   Map<Interger,List<sObject>> indexToObjects = new Map<Interger,List<SObject>>();
    indexToObjects.put(0,cases);
    indexToObjects.put(1,leads);
 
    Interger startingIndex =0;
    System.enqueueJob(new Worker(indexToObjects,startingIndex));
   
   return indexToObjects;

   }

  public static List<Case> getCases(Set<Id> opportunityIds)
  {
    return [select Id,OppOrtunity__c,Status
             From Case WHERE Opportunity__c in : opportunityIds];

  }
 
  public static List<Account> getAccounts(Set<Id> accountIds)
  {
    return [select Id,Name AnnualRevenue from Account
            where Account where Id in : accountIds];
  }

 public static List<Lead> getLeads(Set<Id> leads)
 {
   return [select Id, Name,Status from Lead
           where Id in : leadIds];
 }

}

public without sharing class Worker implements Queueable
{
  Map<Interger,List<sObject>> indexToObjectUpdates;
  Interger index;

  public Worker(Map<Interger,List<sObject>> indexToObjectUpdates,Interger index)
   {
     this.indexToObjectUpdates = indexToObjectUpdates;
     this.index = index;
   }

  public void execute (QueueableContext context)
   {
     if(!indexToObjectUpdates.containskey(index))
      return ;

    update indexToObjectUpdates.get(index);
   
    Interger nextIndex = index +1 ;
    if(indexToObjectUpdates.containskey(nextIndex))
      System.enqueueJob(new Worker(indexToObjectUpdates, nextIndex));

   }
}


@isTest
private class OpportunityTest
{
  public static final Decimal amount = 1000.00;

  @testsetup
  static void setupData()
  {
   
   Account account =new Account();
   account.Name ='Test Account';
   account.AnnualRevenue = 0;
   insert account;
 
   Lead  lead = new Lead();
    lead.Company ='A Company';
    lead.FirstName='Test';
    lead.LastName='Lead';
    lead.Status ='Working - Contacted';
  insert lead;
 
  Opportunity opportunity =new Opportunity();
   opportunity.AccountId = account.Id;
   opportunity.Amount = amount;
   opportunity.Lead__c = lead.Id;
   opportunity.CloseDate = Date.today();
   opportunity.Name= 'Test Opportunity';
   opportunity.StageName ='Negotiation/Review';
   insert opportunity;

  Case caseOnOpportunity =new Case();
  caseOnOpportunity.Opportunity__c=opportunity.Id;
  caseOnOpportunity.Status ='New';
  insert caseOnOpportunity;

  }

 @isTest
  static void cloasedOpportunityTest()
   {
    // Retrieve Opportunity
    List<Opportunity> opportunities = [select Id,AccountId,
      Amount,Lead__c,CloseDate,StageName from Opportunity];

    System.assertEquals(1,opportunities.size(),'Test Opportunity retrieved');
 
    List<Account> accounts = [select Id,Name,AnnualRevenue from account
                             where AnnualRevenue=0];

    System.assertEquals(1, accounts.size(),'1 Account Retrieved with 0 AnnualRevenue');
   
    List<Case> cases = [ select Id, Status from Case
                        where Status != 'Closed'];
    System.assertEquals (1, cases.size(),'1 non-closed Case found');

    List<Lead> leads =[select Id, Name,Status from Lead where Status !='Inactive'];
    System.assertEquals(1,leads.size().'1 active lead found');

   Test.startTest();
     List<Opportunity> oldOpportunities = opportunities;
     List<Opportunity> newOpportunities = new List<Opportunity>();
      //new opp

      Opportunity newOpportunity = new Opportunity();
      newOpportunity.AccountId = opportunities[0].AccountId;
      newOpportunity.Amount = opportunities[0].Amount;
      newOpportunity.Lead__c= opportunities[0].Lead__c;
      newOpportunity.CloseDate = opportunities[0].CloseDate;
      newOpportunity.Id = opportunities[0].Id;
      newOpportunity.StageName = 'Closed Won';

     newOpportunities.add(newOpportunity);
   
     Map<Interger,List<sObject>> workerMap = new Map<Interger,List<sObject>>();
     workerMap = OpportunityUtils.processProducts(oldOpportunities, newOpportunities);
     System.assertEquals(2,workerMap.keySet().size(),'2 Objects set for update');
   
     System.enqueueJob(new Worker(workerMap,1)); // o is enqueued in the function call
 
   Test.stopTest();

  // Then
   List<Account> accountsAfterUpdate = [ select Id,AnnualRevenue from Account
                                        where AnnualRevenue =: amount];
   System.assertEquals(1,accountsAfterUpdate.size(),'1 Account found with expected AnnualRevenue');

  List<Case> casesAfterUpdate = [select Id,Status from Case where Status ='closed'];
 
  System.assertEquals (1,casesAfterUpdate.size(),'1 Closed Case Found');

  List<Lead> leadAfterUpdate =[select Id,Name,Status From Lead where Status ='Inactive'];

  System.assertEquals(1,leadAfterUpdate.size(),'1 Inactive Lead Found');

   }

}


Notes :

1.Queueable Jobs allow use of chaining,  monitoring,and can take any object as input.

2.Jobs have to be defined as a class, unlike future methods, and take more effort.

3.testing Queueable jobs using Test.startTest() and Test.stopTest();

Running Apex on a Schedule :
==============================

we can schedule Apex at regular times or even singular times to execute and get certain tasks done.

Schedule Apex at regular intervals for periodic tasks.

Pair with batch processing for large scale nightly jobs.

UI includes options for Scheduling,or use corn expressions for tighter time increments.

Cron Expressions :
==================

// Seconds Minutes Hours Day_of_month  Day_of_week Optional_year is the default

 public static String cronExpression ='0 0 13 1 * ? '

// The above expression is an example of running on the

1st of the month, every month, at 1PM UTC , no matter the year.


Scheduled tasks are great for
1.Sending emails
2.Data snapshots
3.Accounting
4.Expensive calculations
5.Installed or third party packages.

Limitation of Scheduled Apex :
===============================
100 Scheduled Apex jobs at one time per org.

ex :

// Schedule the Error Checker

string cronExpression = ' 0 0 1 * * ? ';
System.schedule('Error Checker ', cronExpression, new ErrorCheckerScheduled() );


global class ErrorCheckerScheduled implements Schedulable
{
 
  global void execute(SchedulableContext sc)
  {
     checkForErrors();
  }

  public void checkForErrors()
   {
     // Find any errors created today
   
     List<Error__c> errors = getErrors();

    // If any errors exist , notify relevant users
 
    if(!errors.isEmpty())
       notify(errors);

   }

  Public List<Error__c> getErrors()
  {
     Datetime now = Datetime.now();

     Datetime onDayAgo = Datetime.newInstance(now.year(),now.month(),now.day()-1,now.hour(),now.minute(),now.seconds());

    return [ select Id,Name ,CreatedDate from Error__c
              where CreatedDate >= : oneDayAgo ];

  }

  public void notify(List<Error__c> errors)
  {
    // Find Contacts to notify
    List<Contact> contactsNotify = [ select Id, Name, Error_Contact__c from Contact where Error_Contact__c = true ];
   
    // Collect Contact ID

    Set<String> contactIDs = new Set<String>()
 
    for(Contact contact : contactsToNotify)
        contactIDs.add(contact.Id);

    // Construct error message
 
    String errorsInMessage = '';
    String newLine = '/n';

     for(Error__c error : errors)
       errorsInMessage += error.Message__c + newLine;

    // Notify users

    sendErrorEmail(errorsInMessage,contactIDs);

  }

  public void sendErrorEmail (String message, Set<String> contactIDs)
  {
    List<String> contactsToSend = new List<String>();
    contactsToSend.addAll(contactIDs);

   // Construct email message
 
   Messaging.SingleEmailMessage errorEmail = new Messaging.SingleEmailMessage();
   errorEmail.setToAddresses(contactsToSend);
   errorEmail.setPlainTextBody (message);
   errorEmail.setSubject('Errors found on :' + String.valueOf(Date.today()));
 
  // Add to list of emails to send
 
   List<Messaging.Email> emailsToSend = new List<Messaging.Email>();
    emailsToSend.add(errorEmail);

  // Send the email
  Messaging.sendEmail(emailsToSend);

  }

}


@isTest
public class ErrorCheckerTest
{

 @isTest
 static void sendEmailOnExceptionTest()
  {
 
     // Set UserUtils to reflect testing
     // throwing an exception
        UserUtils.testingException = true;
 
     // Get current user
     Id currentUserId = UserInfo.getUserId();
     List<User> currentUser = [select Id,Name,Annual_Revenue__c from User
                               where Id =: currentUserId ];

    Map<Id,Decimal> currentUserToNewAmount = new Map<Id,Decimal>();
    currentUserToNewAmount.put(currentUserId, currentUser[0].Annual_Revenue__c + 5000);
 
    // When :
   
     Test.startTest();
   
     UserUtils.updateAnnualRevenue(currentuserToNewAmount);

     Test.stopTest();

   // Then

   List<Error__c> errorsAfterFuture.size()>0,'Errors found After Future call');
  }

}


Massive Workloads in batch Apex :
=================================
Batch Apex is for high volume of records: from hundreds of thousands to millions.

Each batch job is broken of into three major areas :

a Start function,
an Execute function , and
a Finish function

Governor Limits and Flex Queue :
=================================
Managing governor limits is always important in salesforce and with batch jobs, a key limitation is that only five batch jobs are allowed per org simultaneously.

Note : keep in mind , we could use a  batch to spring off queueable jobs to minimize time spent running the batch job.

Note : five batch jobs per org, but with the Apex Flex Queue, we can expand this to up to 100 queued batch jobs.

Queued batch jobs will have a Holding status when they are added to the queue, and then when exiting the queue, the  indication is a status of Queued.

ex :

global class WelcomeEmailBatch implements Database.Batchable<sObject>
{
   String query = ' select Id,Name,Email,Welcome_Email__c ' +
                  'FROM Contact ' +
                  'WHERE Welcome_Email__c = null '+
                  'AND Email != null ';

  global Database.QueryLocator start(Database.BatchableContext BC)
  {
 
    return Database.getQueryLocator(query);
  }

  global void execute (Database.BatchableContext BC , List<Contact> contacts)
   {
     sendWelcomeEmails(contacts);
   }
  global void finish (Database.BatchableContext BC)
   {
      // Add error handling here for if anything
      // failed in job
   }
  public static void sendWelcomeEmails(List<Contact> contacts)
   {
      List<Contact> contactsToUpdate = new List<Contact>();
       List<Id> contactIds = new List<Id>();
       for(Contact contact : contacts)
        {
           contactIds.add(contact.Id);
         
           contact.Welcome_Email__c = Datetime.now();
            contactsToUpdate.add(contact); 
        }

      Boolean allorNone = true;
      Messaging.MassEmailMessage massEmail = new Messaging.MassEmailMessage();
      massEmail.setTargetObjectIds(contactIds);
      massEmail.setTemplateID(getWelcomeTemplateId());
      Messaging.sendEmail(new List<Messaging.MassEmailMessage>{massEmail},allOrNone);

      if(!contactsToUpdate.isEmpty())
         update contactsToUpdate;
   }

   public static Id getWelcomeTemplateId()
    {
      return [select Id,DeveloperName FROM EmailTemplate
                where DeveloperName ='Welcome'
                LIMIT 1].Id;
    }
}

@isTest
private class WelcomeEmailBatchTest
{
  @testsetup
   static void setupData()
    {
      // create a contact
       insert new Contact (FirstName ='Test',
         LastName ='Contact',
         Email = 'test@example.com',
         Welcome_Email__c=null);

    }

 @isTest
  static void welcomeEmailBatchTest()
   {
    // Given
    List<Contact> contacts = [ select Id,Name,Welcome_Email__c
                               from Contact where Welcome_Email__c = null];
    // when
    Test.startTest();
     Database.executeBatch(new WelcomeEmailBatch());
    Test.stopTest();
   
    // Then
 
   List<Contact> contactsAfterBatch = [ select Id,Name,Welcome_Email__c
                                        FROM Contact
                                        WHERE Welcome_Email__c!=null];
 
  System.assertEquals(1,contactsAfterBatch.size(),'1 Contact Found With Welcome_Email__c populated');
 
   }

}

Apex Flex Queue :
=================
With Flex Queues, any jobs that are submitted for execution but are not processed immediately
by the system go in holding status and are placed in a separate queue (Apex Flex queue).

Up to 100 batch jobs can be in the holding status . When system resources become available, the system picks up jobs from the Apex flex queue and moves them to the batch job queue. The status
of these moved jobs changes from Holding to Queued.Queued jobs get executed when the system is ready to process new jobs and they will then go to InProgress status.

This would give you an impression that a lot of jobs are in holding status and are stuck. They actually are placed in the Apex flex queue and waiting to be picked up when system resources are available.