Trigger: When Opportunity get Closed Send Email to Opportunity Owner

Scenario: When Opportunity get Closed Won send Email to Opportunity Owner.

OpportunityTrigger
trigger OpportunityTrigger on Opportunity (after update) {
    OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
	if(trigger.isAfter && Trigger.isUpdate) {
        handler.sendEmailWhenClosed(trigger.old, Trigger.newMap);
    }
}
OpportunityTriggerHandler
public class OpportunityTriggerHandler {
    public void sendEmailWhenClosed(List<Opportunity> lstOppor, Map<id, Opportunity> opprMap) { 
        for(Id opprId: opprMap.keySet()) { 
            if(opprMap.get(opprId).StageName == 'Closed Won') { 
                sendEmail(opprId);
            }
        }
        List<Opportunity> oppr = [Select Id, OwnerId, StageName From Opportunity Where Id in:lstOppor];
    }
    public void sendEmail(Id OpportunityId) { 
        List<Opportunity> oppr = [Select Id, Name, OwnerId, StageName From Opportunity Where Id =: OpportunityId limit 1];
        List<User> userDetail = [Select UserName, Email From User Where Id=: oppr[0].OwnerId limit 1];
        List<Messaging.Email> emailList = new List<Messaging.Email>();
        Messaging.SingleEmailMessage sendEmail = new Messaging.SingleEmailMessage();
        sendEmail.setToAddresses(new String[] {userDetail[0].Email});
        sendEmail.setSubject('Opportunity ' + oppr[0].Name + ' is Closed Won.');
        String body = 'Dear ' + userDetail[0].UserName + ' Your Opportunity ' + oppr[0].Name + ' is Closed Won';
        sendEmail.setHtmlBody(body);
        emailList.add(sendEmail);
        Messaging.sendEmail(emailList);
    }  
}

How the code works

When an Opportunity is updated and its stage is Closed Won, the code looks up the Opportunity owner and sends them an email through Messaging.SingleEmailMessage.

Problems in this version

  • The email is sent again on every later edit. The code only checks that the stage is Closed Won now. It never checks the old value, so each save of a Closed Won Opportunity sends another email.
  • Queries and emails inside a loop. sendEmail runs two queries and one Messaging.sendEmail call for every Opportunity. Apex allows only 10 sendEmail calls per transaction, so a bulk update fails.
  • An unused query at the end of the first method, which wastes one of the 100 query limit.
  • Wrong greeting. UserName is the login name. Use Name for a friendly greeting.
  • The trigger passes trigger.old. It is better to pass Trigger.new and Trigger.oldMap so the handler can compare old and new stages.

Improved version

trigger OpportunityTrigger on Opportunity (after update) {
    OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
    handler.sendEmailWhenClosed(Trigger.new, Trigger.oldMap);
}
public class OpportunityTriggerHandler {
    public void sendEmailWhenClosed(List<Opportunity> newOpps, Map<Id, Opportunity> oldMap) {
        List<Opportunity> closedWon = new List<Opportunity>();
        for (Opportunity opp : newOpps) {
            Opportunity oldOpp = oldMap.get(opp.Id);
            if (opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won') {
                closedWon.add(opp);
            }
        }
        if (closedWon.isEmpty()) {
            return;
        }
        Set<Id> ownerIds = new Set<Id>();
        for (Opportunity opp : closedWon) {
            ownerIds.add(opp.OwnerId);
        }
        Map<Id, User> owners = new Map<Id, User>([SELECT Id, Name, Email FROM User WHERE Id IN :ownerIds]);
        List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
        for (Opportunity opp : closedWon) {
            User owner = owners.get(opp.OwnerId);
            if (owner == null || String.isBlank(owner.Email)) {
                continue;
            }
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            mail.setToAddresses(new List<String>{ owner.Email });
            mail.setSubject('Opportunity ' + opp.Name + ' is Closed Won');
            mail.setPlainTextBody('Dear ' + owner.Name + ', your Opportunity ' + opp.Name + ' is Closed Won.');
            emails.add(mail);
        }
        if (!emails.isEmpty()) {
            Messaging.sendEmail(emails);
        }
    }
}

This version sends the email only when the stage changes to Closed Won, loads all owners in one query, and sends all emails with one call.

How to test

  1. Move an Opportunity to Closed Won. The owner should receive one email.
  2. Edit the same Opportunity again. No new email should be sent.
  3. Update several Opportunities to Closed Won at once from a list view and check that every owner gets an email.

FAQ

Is there a limit on emails?
Single emails sent from Apex are limited per day for the whole org, so avoid sending emails for every small change.

Could I use a Flow instead?
Yes. A record-triggered Flow with an email alert can do the same without code.

Leave a Reply