Mastering Apex Triggers: A Step-by-Step Guide
In Salesforce, triggers play a crucial role in automating business processes by executing custom actions before or after events such as insertions, updates, or deletions of records. Apex triggers allow developers to add complex logic to record operations, ensuring data integrity and enabling seamless integration with other systems. Understanding various trigger scenarios is essential for implementing efficient and effective solutions in Salesforce. This is part five in the Trigger Scenarios in APEX series. In this post, we will explore different trigger scenarios in Apex, providing practical examples and best practices to help you leverage triggers for automating workflows, enforcing validation rules, and maintaining data consistency. Whether you’re a seasoned developer or just starting with Salesforce, these scenarios will equip you with the knowledge to handle common business requirements and optimize your Salesforce applications. Let’s dive in!
# 1 ~ Develop an Apex trigger that automatically updates the ‘LeadSource’ field on all alternate Contact records associated with an Account whenever the Account record is modified.
In Salesforce, maintaining consistency between parent and child records is key to accurate reporting and seamless data management. A common requirement is to create an Apex trigger that automatically updates the LeadSource
field on all alternate Contact
records linked to an Account
whenever the Account
is modified. This solution highlights the importance of data synchronization, showcasing a scalable and Governor Limit-compliant approach to managing parent-child relationships effectively in Salesforce.
# Apex Class:
public class UpdateContactOnAccountUpdate{
public static void updateContactOnAccUpdateMethod(Map<Id, Account> accMap, Map<Id, Account> accOldMap){
List<Contact> contactToUpdate = New List<Contact>();
List<Contact> conList = [SELECT Id, Name, AccountId FROM Contact WHERE AccountId IN :accMap.keySet()];
for(Integer i = 0; i<conList.size(); i+=2){
Account a = accMap.get(conList[i].AccountId);
if(accMap.containsKey(conList[i].AccountId) && a.Id == conList[i].AccountId && a.Type != accOldMap.get(a.Id).Type){
conList[i].LeadSource = 'Web';
contactToUpdate.add(conList[i]);
}
}
if(!contactToUpdate.isEmpty()){
Update contactToUpdate;
}
}
}
Apex# Apex Trigger:
trigger UpdateContactOnAccountUpdateTrigger on Account (after update) {
UpdateContactOnAccountUpdate.updateContactOnAccUpdateMethod(Trigger.newMap, Trigger.oldMap);
}
Apex# Test Class:
@isTest
public class UpdateContactOnAccountUpdateTest {
@testSetup
public static void setUpData(){
Account a = New Account();
a.Name = 'Test Account';
a.Type = 'Prospect';
insert a;
List<Contact> conList = New List<Contact>();
conList.add(New Contact(FirstName='Test', LastName='Con1', AccountId=a.Id, LeadSource=''));
conList.add(New Contact(FirstName='Test', LastName='Con2', AccountId=a.Id, LeadSource=''));
insert conList;
}
@isTest
public static void updateContactOnAccUpdateMethodTest(){
List<Account> acc = [SELECT Id, Name, Type FROM Account WHERE Name='Test Account' LIMIT 1];
Map<Id, Account> accOldMap = New Map<Id, Account>(acc);
acc[0].Type = 'Direct-Channel';
Update acc;
Map<Id, Account> accNewMap = New Map<Id, Account>(acc);
Test.startTest();
UpdateContactOnAccountUpdate.updateContactOnAccUpdateMethod(accNewMap, accOldMap);
Test.stopTest();
}
}
Apex# 2 ~ Implement an Apex trigger that prevents the creation of more than three Contact records with the same email address within a 24-hour period.
Ensuring data quality is a top priority in Salesforce, especially when managing duplicate records. A common challenge is preventing multiple Contact
records with the same email address from being created in a short timeframe, which can lead to inconsistencies and cluttered data.
This scenario explains how to implement an Apex trigger that restricts the creation of more than three Contact
records with the same email address within a 24-hour period. By leveraging Salesforce’s trigger framework and applying efficient validation logic, this solution helps maintain clean, reliable data while adhering to best practices for performance and Governor Limits.
# Apex Class:
public class PreventMoreThan3ContactInsertion{
public static void preventContactInsertionMethod(List<Contact> conList){
Set<String> emailSet = New Set<String>();
if(!conList.isEmpty()){
for(Contact c : conList){
if(c.Email != Null){
emailSet.add(c.Email);
}
}
}
List<Contact> existingContacts = [SELECT Id, Name, Email FROM Contact WHERE Email != Null AND CreatedDate = TODAY AND Email IN :emailSet];
Map<String, Integer> emailConMap = New Map<String, Integer>();
if(!existingContacts.isEmpty()){
for(Contact c : existingContacts){
if(emailConMap.containsKey(c.Email)){
Integer contactCount = emailConMap.get(c.Email) + 1;
emailConMap.put(c.Email, contactCount);
} else {
emailConMap.put(c.Email, 1);
}
}
}
if(!conList.isEmpty()){
for(Contact cc : conList){
if(cc.Email != Null && emailConMap.containsKey(cc.Email) && emailConMap.get(cc.Email)>= 3){
cc.addError('Cannot insert more than 3 contacts with the same email in a day');
}
}
}
}
}
Apex# Apex Trigger:
trigger PreventMoreThan3ContactInsertionTrigger on Contact (before insert, after undelete, before update) {
if(Trigger.isBefore && (Trigger.isInsert || Trigger.isUpdate)){
PreventMoreThan3ContactInsertion.preventContactInsertionMethod(Trigger.new);
}
if(Trigger.isAfter && Trigger.isUndelete){
PreventMoreThan3ContactInsertion.preventContactInsertionMethod(Trigger.new);
}
}
Apex# Test Class:
@isTest
public class PreventMoreThan3ContactInsertionTest {
@testSetup
public static void setUpData(){
List<Contact> conList = New List<Contact>();
conList.add(New Contact(FirstName='Test', LastName='Con1', Email='test@gmail.com'));
conList.add(New Contact(FirstName='Test', LastName='Con2', Email='test@gmail.com'));
conList.add(New Contact(FirstName='Test', LastName='Con3', Email='test@gmail.com'));
insert conList;
}
@isTest
public static void preventContactInsertionMethodTest(){
List<Contact> cList = New List<Contact>();
cList.add(New Contact (FirstName='Test', LastName='Con4', Email='test@gmail.com'));
Test.startTest();
try {
insert cList;
System.assert(false, 'Expected exception was not thrown');
} catch (DmlException e) {
// Assert that the error message is correct
System.assert(e.getMessage().contains('Cannot insert more than 3 contacts with the same email in a day'));
}
Test.stopTest();
// Verify the number of contacts in the database
Integer contactCount = [SELECT COUNT() FROM Contact WHERE Email = 'test@gmail.com'];
System.assertEquals(contactCount, 3, 'There should only be 3 contacts with the same email');
}
@isTest
public static void preventContactInsertionMethodTest1(){
Contact c = [SELECT Id, Name FROM Contact WHERE LastName='Con3'];
Delete c;
Contact cc = New Contact();
cc.LastName = 'Con5';
cc.FirstName = 'Test';
cc.Email = 'test@gmail.com';
insert cc;
List<Contact> cList = [SELECT Id, Name FROM Contact WHERE LastName='Con3' ALL ROWS];
Test.startTest();
try {
Undelete cList;
System.assert(false, 'Expected exception was not thrown');
} catch (DmlException e) {
// Assert that the error message is correct
System.assert(e.getMessage().contains('Cannot insert more than 3 contacts with the same email in a day'));
}
Test.stopTest();
// Verify the number of contacts in the database
Integer contactCount = [SELECT COUNT() FROM Contact WHERE Email = 'test@gmail.com'];
System.assertEquals(contactCount, 3, 'There should only be 3 contacts with the same email');
}
}
Apex# 3 ~ Create an Apex trigger that, upon an Account record being updated to an inactive status, automatically reviews all associated Opportunities. If the creation date of an Opportunity is more than 30 days prior to the Account’s inactivation, the trigger should update the Opportunity’s Stage to ‘Closed Lost’.
Managing Opportunities effectively is crucial when dealing with inactive Accounts in Salesforce. When an Account
transitions to an inactive status, it’s often necessary to review and update associated Opportunities
to reflect the change in business status.
In this post, we’ll demonstrate how to create an Apex trigger that automates this process. When an Account
is marked as inactive, the trigger checks all related Opportunities
. If an Opportunity
was created more than 30 days before the inactivation, its Stage
is updated to Closed Lost
. This solution streamlines pipeline management and ensures data reflects the latest business reality, all while adhering to Salesforce best practices for performance and scalability.
# Apex Class:
public class UpdateOppOnAccUpdate {
public static void updateOppOnAccUpdateMethod(Map<Id, Account> accMap, Map<Id, Account> accOldMap){
List<Opportunity> oppList = New List<Opportunity>();
/* Since we can't manually assign CreatedDate value in our data that we create for writing test class, we have to make use of "test.isRunningTest()"
* to bypass the CreatedDate condition in our query in our original class.
*/
if(!test.isRunningTest()){
oppList = [SELECT Id, Name, AccountId, CreatedDate FROM Opportunity WHERE AccountId IN :accMap.keySet() AND CreatedDate < Last_N_Days:31];
}else{
oppList = [SELECT Id, Name, AccountId, CreatedDate FROM Opportunity WHERE AccountId IN :accMap.keySet()];
}
if(!oppList.isEmpty()){
for(Opportunity opp : oppList){
Account a = accMap.get(opp.AccountId);
/* Similarly if the CreatedDate condition was part of the "if condition" (line 24) then "test.isRunningTest()" would be used in the OR part(||) of the "if condition"
* along with the rest of the conditions written as it is in a separate bracket.
*
* if((accMap.containsKey(opp.AccountId) && a.Id == opp.AccountId && a.Active__c != accOldMap.get(opp.AccountId).Active__c && a.Active__c == 'No') || test.isRunningTest())
*/
if(accMap.containsKey(opp.AccountId) && a.Id == opp.AccountId && a.Active__c != accOldMap.get(opp.AccountId).Active__c && a.Active__c == 'No'){
opp.StageName = 'Closed Lost';
}
}
}
if(!oppList.isEmpty()){
update oppList;
}
}
}
Apex# Apex Trigger:
trigger UpdateOppOnAccUpdateTrigger on Account (after update) {
if(Trigger.isAfter && Trigger.isUpdate){
UpdateOppOnAccUpdate.updateOppOnAccUpdateMethod(Trigger.newMap, Trigger.oldMap);
}
}
Apex# Test Class:
@isTest
public class UpdateOppOnAccUpdateTest {
@testSetup
public static void setUpData(){
Account a = New Account();
a.Name = 'Test Account';
a.Active__c = 'Yes';
insert a;
List<Opportunity> oppList = New List<Opportunity>();
oppList.add(New Opportunity(Name='Test Opp1', CloseDate=System.TODAY(), AccountId=a.Id, StageName='Prospecting'));
oppList.add(New Opportunity(Name='Test Opp2', CloseDate=System.TODAY(), AccountId=a.Id, StageName='Prospecting'));
insert oppList;
}
@isTest
public static void updateOppOnAccUpdateMethodTest(){
Account acc = [SELECT Id, Name, Active__c FROM Account WHERE Name='Test Account' LIMIT 1];
acc.Active__c = 'No';
Test.startTest();
Update acc;
Test.stopTest();
List<Opportunity> oppList = [SELECT Name, AccountId, StageName FROM Opportunity WHERE AccountId =:acc.Id];
System.assertEquals('Closed Lost', oppList[0].StageName);
System.assertEquals('Closed Lost', oppList[1].StageName);
}
}
Apex