Loading
Nonprofit Success Pack (NPSP) Managed Package
Deploy a Custom Apex Class in the TDTM Framework for NPSP

Deploy a Custom Apex Class in the TDTM Framework for NPSP

The Nonprofit Success Pack (NPSP) relies heavily on Apex to implement much of its functionality. Salesforce.org products use one trigger per object and Table-Driven Trigger Management (TDTM) to control the execution of Apex classes.

You should familiarize yourself with the TDTM architecture if you’re developing your own custom Apex code, or integrating an external system with Salesforce. You don’t need to create new triggers for common standard objects such as Contacts, Accounts, and Opportunities or for NPSP custom objects. TDTM is extensible. When you write your own code, you only need to write Apex classes within the TDTM framework.

Note
Note If you need a refresher on the fundamentals of TDTM, read Table-Driven Trigger Management Overview.
  • Summary of Steps
    Here's an overview of the steps to deploy your custom Apex class in the TDTM framework.
  • Technical Overview
    Learn how NPSP triggers work and the logic behind them.
  • Create an Apex Class
    Here's an example of a custom class that follows the TDTM design.
  • Create a Test Class
    By default, test classes can't see data in your org. Information about your Trigger Handler record resides in the Trigger Handler object and as a result, you must load the cached Trigger Handlers into memory.
  • Create a Trigger Handler Record
    When you create a custom class of your own, you must also create a Trigger_Handler__c record that references the class.
  • Using the TDTM_RunnableMutable Interface
    Custom implementations of TDTM can't interact with the TDTM_Runnable.DmlWrapper collection when it already has records in the objectsToUpdate collection. If you try to include your own DML in the objectsToUpdate collection, you'll get a Duplicate id in list error. While it's possible to run your code asynchronously or perform DML separately from theTDTM_TriggerHandler, consider using an alternative interface called TDTM_RunnableMutable.

Summary of Steps

Here's an overview of the steps to deploy your custom Apex class in the TDTM framework.

  1. Create your Apex class. The class must:
    1. Extend the npsp.TDTM_Runnable class. Depending on your implementation details, you may also use the TDTM_RunnableMutable interface.
    2. Override the TDTM_Runnable run method, which returns a npsp.TDTM_Runnable.DmlWrapper and takes the following parameters:
      1. List<SObject> newlist
      2. List<SObject> oldlist
      3. npsp.TDTM_Runnable.Action triggerAction
      4. Schema.DescribeSObjectResult objResult
    3. Use DMLWrapper for efficient DML operations.
  2. Write your test class.
  3. Add a Trigger Handler record with the appropriate field data NPSP Settings | System Tools | Trigger Configuration.
Tip
Tip As a best practice, use _TDTM as the suffix for your class (example: OPP_MyAwesomeClass_TDTM.cls)

Technical Overview

Learn how NPSP triggers work and the logic behind them.

In TDTM design, our triggers call the TDTM_TriggerHandler class and pass it all the environment information. The actual business logic that needs to run when an action occurs on a record is stored in plain old classes. We created a custom object, Trigger_Handler__c, to store which classes should run for each object, along with the related actions. In this object, we also define whether the class is active or inactive, what order it should execute in for the same object, and other settings. The Trigger Handler then calls these classes when appropriate, which provides the advantage of running all the DML operations at the end of execution through the DmlWrapper class. For more information about the Trigger Handler object, read Manage Trigger Handlers.

Note
Note Our codebase is open source. Explore the Trigger Handler class and see how our package code implements this framework in our GitHub repository. NPSP is referred to as Cumulus in Git. Cumulus is a public repository and you can contribute code if you wish. For information on contributing code, see our code contributor's guide.

Not every Salesforce object has a TDTM trigger. You can see which objects have a TDTM trigger in Apex Class Descriptions for NPSP.

To see all Apex triggers present in your org, go to Apex Triggers in Setup.

If you create your own custom objects, or want to run a TDTM class on a standard object that doesn't have a trigger provided by the package, you can create a trigger within the TDTM framework. Use the TDTM_Config_API global class and reference the npsp namespace. Here’s an example code snippet:

trigger TDTM_MyCustomObject on MyCustomObject__c (after delete, after insert, after undelete,after update, before delete, before insert, before update) {

npsp.TDTM_Config_API.run(Trigger.isBefore, Trigger.isAfter, Trigger.isInsert, Trigger.isUpdate, Trigger.isDelete, Trigger.isUndelete, Trigger.new, Trigger.old, Schema.SObjectType.MyCustomObject__c);
}

Create an Apex Class

Here's an example of a custom class that follows the TDTM design.

global class CM_ContactRecentCampaign_TDTM extends npsp.TDTM_Runnable {

    global override npsp.TDTM_Runnable.DmlWrapper run(List<SObject> newlist,
                                             List<SObject> oldlist,
                                             npsp.TDTM_Runnable.Action triggerAction,
                                             Schema.DescribeSObjectResult objResult) {

        npsp.TDTM_Runnable.dmlWrapper dmlWrapper = new npsp.TDTM_Runnable.DmlWrapper();
        List<Contact> contactsToUpdate = new List<Contact>();
        List<CampaignMember> newCMList = (List<CampaignMember>) newlist;
        List<CampaignMember> oldCMList = (List<Campaignmember>) oldlist;

        if (triggerAction == npsp.TDTM_Runnable.Action.AfterInsert) {

          for (CampaignMember cm : newCMList) {
            if (cm.Status == 'Responded') {
              contactsToUpdate.add(new Contact(Id = cm.ContactId, Most_Recent_Responded_Campaign__c = cm.CampaignId));
            }
          }
        }

        else if (triggerAction == npsp.TDTM_Runnable.Action.AfterUpdate) {

          for (Integer x = 0; x < newCMList.size(); x++) {
            if (newCMList[x].Status == 'Responded' && (newCMList[x].Status <> oldCMList[x].Status) ) {
              contactsToUpdate.add(new Contact(Id = newCMList[x].ContactId, Most_Recent_Responded_Campaign__c = newCMList[x].CampaignId));
            }
          }
        }

        dmlWrapper.objectsToUpdate.addAll((List<Contact>)contactsToUpdate);
      return dmlWrapper;
    }
}

Note that because the class is external you need to use the npsp prefix when calling classes inside the NPSP package. Additionally, if the class you are writing is inside another managed package, include the package prefix when entering the class name in the Class__c field of the Trigger Handler record. In our example, if CM_ContactRecentCampaign_TDTM was inside a managed package with prefix foo, its name should be entered as foo.CM_ContactRecentCampaign_TDTM.

Create a Test Class

By default, test classes can't see data in your org. Information about your Trigger Handler record resides in the Trigger Handler object and as a result, you must load the cached Trigger Handlers into memory.

  1. Use the global class TDTM_Config_API and its method getCachedRecords.
    List<npsp__Trigger_Handler__c> listHandlers = npsp.TDTM_Config_API.getCachedRecords();
  2. Add information about your Trigger Handler. For example:
    npsp__Trigger_Handler__c th = new npsp__Trigger_Handler__c();
    th.Name = 'MyOppAwesomeClass_TDTM';
    th.npsp__Class__c = 'OPP_MyAwesomeClass_TDTM';
    th.npsp__Object__c = 'Opportunity';
    th.npsp__Trigger_Action__c = 'AfterInsert';
    th.npsp__Active__c = true;
    th.npsp__Load_Order__c = 1;
    th.npsp__Asynchronous__c = false;			  
    Important
    Important You must declare npsp__Asynchronous__c to be false or true in your test class. A null value causes the test to fail.
  3. Add your Trigger Handler to the cached records:
    listHandlers.add(th);
  4. Insert your data and test your Trigger Handler:
    // setup our test data...
      test.startTest();
    // do some operations...
      test.stopTest();
    // validate our results...

Here’s an example of a test class that follows TDTM design:

@isTest
private class CM_ContactRecentCampaign_TEST {
	static testMethod void test_CM_Responded_TDTM() {

		// Retrieve default NPSP Trigger Handlers
		List<npsp__Trigger_Handler__c> triggerHandlers = npsp.TDTM_Config_API.getCachedRecords();

		// Add our Trigger Handler to cached Trigger Handlers
		npsp__Trigger_Handler__c th = new npsp__Trigger_Handler__c();
		th.Name = 'MyCMTriggerHandler';
		th.npsp__Class__c = 'CM_ContactRecentCampaign_TDTM';
		th.npsp__Object__c = 'CampaignMember';
		th.npsp__Trigger_Action__c = 'AfterInsert;AfterUpdate;';
		th.npsp__Active__c = true;
		th.npsp__Load_Order__c = 1;
		th.npsp__Asynchronous__c = false;

		triggerHandlers.add(th);
		
    // set up test data
		Contact con1 = new Contact(FirstName = 'Jess', LastName = 'Lopez');
		insert con1;

		List<Campaign> campList = new List<Campaign>();
    campList.add(new Campaign(Name = 'Test Campaign 1', IsActive = true));
    campList.add(new Campaign(Name = 'Test Campaign 2', IsActive = true));
		insert campList;

    List<CampaignMember> campMemberList = new List<CampaignMember>();
    campMemberList.add(new CampaignMember(ContactId = con1.Id, CampaignId = campList[0].Id, Status = 'Responded'));
    campMemberList.add(new CampaignMember(ContactId = con1.Id, CampaignId = campList[1].Id, Status = 'Sent'));
    insert campMemberList;
		
test.startTest();

// Test 1: Insert 'Responded' Campaign Member and verify Contact update.
con1 = [SELECT Id, Most_Recent_Responded_Campaign__c FROM Contact WHERE Id = :con1.Id];
	System.assertEquals(con1.Most_Recent_Responded_Campaign__c, campList[0].Id);

       // Test 2: Change Status from 'Sent' to 'Responded' and verify Contact update
       campMemberList[1].Status = 'Responded';
       update campMemberList[1];
       con1 = [SELECT Id, Most_Recent_Responded_Campaign__c FROM Contact WHERE Id = :con1.Id];
			System.assertEquals(con1.Most_Recent_Responded_Campaign__c, campList[1].Id);

	test.stopTest();
	}
}

Create a Trigger Handler Record

When you create a custom class of your own, you must also create a Trigger_Handler__c record that references the class.

Read Manage Trigger Handlers in NPSP for more information.

There must be a Trigger_Handler__c record for each class managed by TDTM. Take a look at the Trigger Configuration page in NPSP Settings | System Tools | Trigger Configuration to see the list of Trigger_Handler__c records in your org.

Trigger configuration page in NPSP Settings

Load Order is important in TDTM. If you want your code to fire after NPSP packaged code, choose a number after the last number used for the object. For example, you can use 3 for a custom Account class. Your class can also run in parallel with another class. For example, you can use 2 for a custom Contact class. However, you won't know for sure if your class or the packaged classes run first. If execution order precision is needed, it’s best to design your class to run after the packaged classes.

Using the TDTM_RunnableMutable Interface

Custom implementations of TDTM can't interact with the TDTM_Runnable.DmlWrapper collection when it already has records in the objectsToUpdate collection. If you try to include your own DML in the objectsToUpdate collection, you'll get a Duplicate id in list error. While it's possible to run your code asynchronously or perform DML separately from theTDTM_TriggerHandler, consider using an alternative interface called TDTM_RunnableMutable.

SetFieldsOnAccount Apex Class

Note
Note Custom implementations of TDTM might break if NPSP updates its TDTM logic to start adding records into objectsToUpdate where it had not before. Use the TDTM_MutableRunnable interface in your custom Apex to avoid this possibility.

The code signature for implementing TDTM_RunnableMutable is very similar to TDTM_Runnable. Let's look at a code example.

In this example, we want the NumberofEmployees field on the Account object to increase by one every time a Contact is created for a given Account.

global class CON_SetFieldsOnAccount_TDTM_Mutable extends npsp.TDTM_RunnableMutable {
  
  global override void run(List<SObject> listNew, List<SObject> listOld,
            npsp.TDTM_Runnable.Action triggerAction, Schema.DescribeSObjectResult objResult,
            npsp.TDTM_Runnable.DmlWrapper dmlWrapper) {
    // cast new contacts
    List<Contact> newRecords = (List<Contact>) listNew;

    if(triggerAction == npsp.TDTM_Runnable.Action.AfterInsert) {
      // create collection to hold Account IDs
      Set<Id> accountIds = new Set<Id>();

      // iterate over new records to collect Account IDs
      for(Contact newRecord : newRecords) {
        accountIds.add(newRecord.AccountId);
      }

      // retrieve those accounts
      Map<Id, Account> accounts = getAccounts(accountIds);

      // create a map version of dmlWrapper's objectsToUpdate
      Map<Id, SObject> objectsToUpdateMap = new Map<Id, SObject>(dmlWrapper.objectsToUpdate);

      // iterate over contacts to set each Account's NumberOfEmployees
      for(Contact newRecord : newRecords) {
        if(accounts.containsKey(newRecord.AccountId)) {
          /*
            check whether this account is already in dmlWrapper
              as a record that will receive an update
          */
          Account account;

          if(objectsToUpdateMap.containsKey(newRecord.AccountId)) {
            account = (Account) objectsToUpdateMap.get(newRecord.AccountId);
          } else {
            account = accounts.get(newRecord.AccountId);
          }
          
          // if this is null, initialize to 0
          if(account.NumberOfEmployees == null) {
            account.NumberOfEmployees = 0;
          }

          // increment by 1, as this contact is part of the account
          account.NumberOfEmployees += 1;

          // put this account into the collection of records to update
          objectsToUpdateMap.put(account.Id, account);
        }
      }

      // update dmlWrapper's objectsToUpdate to reflect this collection
      dmlWrapper.objectsToUpdate = objectsToUpdateMap.values();
    }
  }

  public static Map<Id, Account> getAccounts(Set<Id> recordIds) {
    return new Map<Id, Account>([SELECT Id, NumberOfEmployees FROM Account WHERE Id IN :recordIds]);
  }
}

RunnableMutable Trigger Handler

In this code example, we see that the CON_SetFieldsOnAccount_TDTM_Mutable class extends TDTM_RunnableMutable, not TDTM_Runnable. The class checks to see if the Account record from the trigger is already in dmlWrapper.objectsToUpdate and updates the record, or places the Account retrieved into that collection.

Create a Trigger Handler record for each class that implements TDTM_RunnableMutable. Using the CON_SetFieldsOnAccount_TDTM_Mutable class example, the trigger handler would look like this:

npsp__Trigger_Handler__c triggerHandler = new npsp__Trigger_Handler__c();
triggerHandler.Name = 'CON_SetFieldsOnAccount_TDTM_Mutable';
triggerHandler.npsp__Active__c = true;
triggerHandler.npsp__Asynchronous__c = false;
triggerHandler.npsp__Class__c = 'CON_SetFieldsOnAccount_TDTM_Mutable';
triggerHandler.npsp__Load_Order__c = 2;
triggerHandler.npsp__Object__c = 'Contact';
triggerHandler.npsp__Trigger_Action__c = 'AfterInsert';
insert triggerHandler;
Note
Note Special thanks to Roger Mitchell, Nicolas Campbell, and Matthew Poe for their design work and implementation of the RunnableMutable feature.
 
Cargando
Salesforce Help | Article