function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Ryan-HaireRyan-Haire 

My First (useful) Trigger help

I'd like to write a trigger so when an opportunity get's submitted it creates a new record on a custom object for suppression/tracking purposes.

 

The custom object is suppression__c

 

Client-  Master-Detail with Account
Company name-  (pull from Account on Op)
Email domain- (Pull from main contact on Op)
Opportunity-  (Link to Opportunity)
Website- (Pull from Account on Op)
Source- (Picklist- Will always be "QSO")
Type- (Picklist- will always be "Company")

 

Seems like it should be easy enough to do, but can someone point me in the right direction on how to get started?

 

Thanks-

Ryan

Best Answer chosen by Admin (Salesforce Developers) 
Ryan-HaireRyan-Haire

 

I think I have the code working the way I'd like it to. Onto the TestClass which of course is giving me some issues.

 

Here's the trigger

trigger syncSuppression on Opportunity (after update) {
    List<suppression__c> suppressionList = new List<suppression__c>();
    //We'll contain the list of Ids that we need to manage, since not every opportunity necessarily needs to be processed this
    //may be different than what's in the trigger.new value
    Set<Id> opportunitiesToProcess = new Set<Id>{};
    for (Opportunity o:trigger.new) {        
        //Check if the latest opportunity's stage name is 'Submitted' since this is the only one that needs to be pushed into the suppression object
        //Since we only want to do this on the "first" time it's become submitted we'll check first
        //1) If Trigger.isInsert is true, then the trigger is currently firing due to an "insert" operation of an opportunity
        //2) If it's not an insert, then we are getting the OLD record from the oldMap. The oldMap is populated during update operations
        //     and will contain the value of the records before the update occurred
        //     So we'll check the old and see if the stage name is not submitted, if it wasn't we can assume since the new one is submitted it's being changed
       if (o.StageName == 'Submitted'  && 
           (Trigger.isInsert || Trigger.oldMap.get(o.Id).StageName != 'Submitted'))
        {
            opportunitiesToProcess.add(o.Id);
        }        
    }
    
    //If there are no opportunities that match the criteria above we might as well break out and not do any more processing
    //! is the NOT operation for the value, so basically it's saying if the lits of opportunities to process is not empty
    //You can also read it like this:
    //opportunitiesToProcess.isEmpty() == false
    if (!opportunitiesToProcess.isEmpty())
    {
        //Here is where you can get the information for the primary campaign's client account
        for (Opportunity o : [ select Id, ClientID__c, Campaign.Client_Account__r.id, Account.Name, Account.Website, Main_Contact__r.Email_Domain__c from Opportunity where Id IN :opportunitiesToProcess ])
        {
            //Do stuff here
            suppression__C newSuppression = new suppression__c();
            newSuppression.Client__c=o.Campaign.Client_Account__r.id;
            newSuppression.Company_Name__c=o.Account.Name;
            newSuppression.website__c=o.Account.Website;
            newSuppression.email_domain__c=o.Main_Contact__r.Email_Domain__c;
            suppressionList.add(newSuppression);        
        }
                
        insert suppressionList;        
    }
}

 And Here's the TestClass. I'm getting a "

System.DmlException: Update failed. First exception on row 0 with id 006Q000000BVhGAIA1; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, syncSuppression: execution of AfterUpdate

caused by: System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Client__c]: [Client__c]

Trigger.syncSuppression: line 38, column 1: []"

 

So what I'm trying  to do is create a new opportunity with one stagename and then update the opportunity stagename to Submitted to fire the trigger. It says the code has 100% coverage, but then i get an error on the Tests column of developer console.

 

@isTest

private class syncSuppressionTest {

static testMethod void validateSyncSuppression(){
// new opportunity
Opportunity o = new Opportunity(
Name='testsync', 
StageName='0 BANT',
Type='New Client QSO Opportunity',
CloseDate= date.today(),
CampaignID='701Q0000000Ofq8',
Main_Contact__c= '003Q000000hv2yI'
);
System.debug('new op made: ' + o.name);

//insert op
insert o;

//update op
o.StageName='Submitted';
update o;
System.debug('new op updated: ' + o.StageName);
    
}

}

All Answers

Sean TanSean Tan

Something like this should get you in the right direction:

 

trigger SyncOpportunityToSupression on Opportunity (after insert) {

    Suppression__c[] suppressionList = new Suppresion__c[]{};
    
    for (Opportunity item : [SELECT Id, Account.Name, Account.Website, (SELECT Contact.Email FROM OpportunityContactRoles WHERE IsPrimary = true) FROM Opportunity WHERE Id in :Trigger.newMap.keySet()])
    {
        Suppression__c newItem = new Suppression__c();
        if (item.OpportunityContactRoles != null && !OpportunityContactRoles.isEmpty())
        {            
            //Sync lead contact info here
            Contact c = item.OpportunityContactRoles[0].Contact;
        }
        //Sync other fields here here...        
        newItem.Company_Name__c = item.Account.Name;    
        //etc...
        suppressionList.add(newItem);
    }
    
    if (!suppressionList.isEmpty())
    {
        insert suppressionList;
    }
}

 You can also probably get rid of some of your flattened fields (such as Account name) via adding a formula field to your object instead.

Ryan-HaireRyan-Haire

Thank you so much-- this was a great start and I'm starting to get the trigger working-- Your code pointed me in the right direction on where to go and now I think I'm getting it-- A few questions:

 

1) In Opportunities we attach ops to a primary campaign source- In primary campaign source is a Client field. Is it possible to access that field in a trigger without having to make a lookup formula field on the op? so something like this: o.Campaign.Client_Account__r.id;(this didn't work) instead of having to make a formula field in an opportunity that just pulls the Client Account ID in the primary campaign in.

2) This trigger has to fire when the opportunity is submitted for approval. There is a Stage called Submitted. Can I program the trigger to fire only the first time the Stage hits Submitted?

 

Thanks so much for the help-

 

Ryan

 

trigger syncSuppression on Opportunity (after insert) {


List<suppression__c> suppressionList = new List<suppression__c>();
    for (Opportunity o:trigger.new){
        suppression__C newSuppression = new suppression__c();
        newSuppression.client__c=o.ClientID__c;
        
        suppressionList.add(newSuppression);
    insert suppressionList;
    }
}

 

 

Sean TanSean Tan

1) So on the Campaign object you have a custom field called Client_Account__c? If you are trying to access any parent / child table information it will require another query to the database to get that.

2) Yes you can, so instead of just after insert you want it on after update as well to determine the stage name.

 

Try this:

 

trigger syncSuppression on Opportunity (after insert, after update) {
    
    List<suppression__c> suppressionList = new List<suppression__c>();
    Set<Id> opportunitiesToProcess = new Set<Id>{};
    for (Opportunity o:trigger.new) {        
        if (o.StageName == 'Submitted' &&
           (Trigger.isInsert || Trigger.oldMap.get(o.Id).StageName != 'Submitted'))
        {
            opportunitiesToProcess.add(o.Id);
        }        
    }
    
    //Query here to get the information you need that you need to sync
    if (!opportunitiesToProcess.isEmpty())
    {
        //Here is where you can get the information for the primary campaign's client account
        for (Opportunity o : [ select Id, ClientID__c, Campaign.Client_Account__r.Id from Opportunity where Id IN :opportunitiesToProcess ])
        {
            //Do stuff here
            suppression__C newSuppression = new suppression__c();
            newSuppression.client__c=o.ClientID__c;        
            suppressionList.add(newSuppression);        
        }
                
        insert suppressionList;        
    }
}

 

 

 

 

Ryan-HaireRyan-Haire

Thank you so much. Yes. On the Campaign Object there is a Client_Account__c field that I'm trying to get at without having to make custom formula fields for every field I want to import. Trying to learn the correct way to program this, even if it's more difficult on the front end so sorry if the questions are really basic. This is my first venture into programming.

 

So if I'm understanding this code right, is the "if (!opportunitiesToProcess.isEmpty()" saying ! bind to the opportunitiesToProcess Set above, check and see if it has anything in it, and if it's empty fill it with the query here.

 

Then we create a For Loop to get all of the essential fields back from the Opportunity, but by creating a set we're able to get at the Parent/child info? So I can query the __r piece of the field?

 

Can you explain what this code is doing? Just not sure mainly what the Trigger.isInsert || Trigger.oldMap line is saying.

if (o.StageName == 'Submitted' &&
           (Trigger.isInsert || Trigger.oldMap.get(o.Id).StageName != 'Submitted'))
        {
            opportunitiesToProcess.add(o.Id);
        }

 

 

Just trying to understand exactly what's going on with this- Sorry if it's really basic but this is a HUGE help.

 

Ryan

 

 

 

Sean TanSean Tan

Not a problem, here's a revised version with some comment lines to help you understand the flow of what's happening and what I'm doing:

 

trigger syncSuppression on Opportunity (after insert, after update) {
    
    List<suppression__c> suppressionList = new List<suppression__c>();
    //We'll contain the list of Ids that we need to manage, since not every opportunity necessarily needs to be processed this
    //may be different than what's in the trigger.new value
    Set<Id> opportunitiesToProcess = new Set<Id>{};
    for (Opportunity o:trigger.new) {        
        //Check if the latest opportunity's stage name is 'Submitted' since this is the only one that needs to be pushed into the suppression object
        //Since we only want to do this on the "first" time it's become submitted we'll check first
        //1) If Trigger.isInsert is true, then the trigger is currently firing due to an "insert" operation of an opportunity
        //2) If it's not an insert, then we are getting the OLD record from the oldMap. The oldMap is populated during update operations
        //     and will contain the value of the records before the update occurred
        //     So we'll check the old and see if the stage name is not submitted, if it wasn't we can assume since the new one is submitted it's being
// changed
        if (o.StageName == 'Submitted' &&
           (Trigger.isInsert || Trigger.oldMap.get(o.Id).StageName != 'Submitted'))
        {
            opportunitiesToProcess.add(o.Id);
        }        
    }
    
    //If there are no opportunities that match the criteria above we might as well break out and not do any more processing
    //! is the NOT operation for the value, so basically it's saying if the lits of opportunities to process is not empty
    //You can also read it like this:
    //opportunitiesToProcess.isEmpty() == false
    if (!opportunitiesToProcess.isEmpty())
    {
        //Here is where you can get the information for the primary campaign's client account
        for (Opportunity o : [ select Id, ClientID__c, Campaign.Client_Account__r.Id from Opportunity where Id IN :opportunitiesToProcess ])
        {
            //Do stuff here
            suppression__C newSuppression = new suppression__c();
            newSuppression.client__c=o.ClientID__c;        
            suppressionList.add(newSuppression);        
        }
                
        insert suppressionList;        
    }
}

 

Ryan-HaireRyan-Haire

Thanks for the quick response and breakdown. Makes total sense and will be a great reference to go back to. I'm going to work on it more tomorrow and see if I can't get this thing firing the way I want it.

 

Watching the Dreamforce Apex Trigger intro also helped alot.

 

Definitely no substitute for just diving in and seeing how many error codes I can throw.

 

 

Ryan-HaireRyan-Haire

 

I think I have the code working the way I'd like it to. Onto the TestClass which of course is giving me some issues.

 

Here's the trigger

trigger syncSuppression on Opportunity (after update) {
    List<suppression__c> suppressionList = new List<suppression__c>();
    //We'll contain the list of Ids that we need to manage, since not every opportunity necessarily needs to be processed this
    //may be different than what's in the trigger.new value
    Set<Id> opportunitiesToProcess = new Set<Id>{};
    for (Opportunity o:trigger.new) {        
        //Check if the latest opportunity's stage name is 'Submitted' since this is the only one that needs to be pushed into the suppression object
        //Since we only want to do this on the "first" time it's become submitted we'll check first
        //1) If Trigger.isInsert is true, then the trigger is currently firing due to an "insert" operation of an opportunity
        //2) If it's not an insert, then we are getting the OLD record from the oldMap. The oldMap is populated during update operations
        //     and will contain the value of the records before the update occurred
        //     So we'll check the old and see if the stage name is not submitted, if it wasn't we can assume since the new one is submitted it's being changed
       if (o.StageName == 'Submitted'  && 
           (Trigger.isInsert || Trigger.oldMap.get(o.Id).StageName != 'Submitted'))
        {
            opportunitiesToProcess.add(o.Id);
        }        
    }
    
    //If there are no opportunities that match the criteria above we might as well break out and not do any more processing
    //! is the NOT operation for the value, so basically it's saying if the lits of opportunities to process is not empty
    //You can also read it like this:
    //opportunitiesToProcess.isEmpty() == false
    if (!opportunitiesToProcess.isEmpty())
    {
        //Here is where you can get the information for the primary campaign's client account
        for (Opportunity o : [ select Id, ClientID__c, Campaign.Client_Account__r.id, Account.Name, Account.Website, Main_Contact__r.Email_Domain__c from Opportunity where Id IN :opportunitiesToProcess ])
        {
            //Do stuff here
            suppression__C newSuppression = new suppression__c();
            newSuppression.Client__c=o.Campaign.Client_Account__r.id;
            newSuppression.Company_Name__c=o.Account.Name;
            newSuppression.website__c=o.Account.Website;
            newSuppression.email_domain__c=o.Main_Contact__r.Email_Domain__c;
            suppressionList.add(newSuppression);        
        }
                
        insert suppressionList;        
    }
}

 And Here's the TestClass. I'm getting a "

System.DmlException: Update failed. First exception on row 0 with id 006Q000000BVhGAIA1; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, syncSuppression: execution of AfterUpdate

caused by: System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Client__c]: [Client__c]

Trigger.syncSuppression: line 38, column 1: []"

 

So what I'm trying  to do is create a new opportunity with one stagename and then update the opportunity stagename to Submitted to fire the trigger. It says the code has 100% coverage, but then i get an error on the Tests column of developer console.

 

@isTest

private class syncSuppressionTest {

static testMethod void validateSyncSuppression(){
// new opportunity
Opportunity o = new Opportunity(
Name='testsync', 
StageName='0 BANT',
Type='New Client QSO Opportunity',
CloseDate= date.today(),
CampaignID='701Q0000000Ofq8',
Main_Contact__c= '003Q000000hv2yI'
);
System.debug('new op made: ' + o.name);

//insert op
insert o;

//update op
o.StageName='Submitted';
update o;
System.debug('new op updated: ' + o.StageName);
    
}

}
This was selected as the best answer
Sean TanSean Tan

Chances are your test class is running without seeing all data in the org. You can work around this by setting the see all data tag to true:

 

@isTest(SeeAllData=true)
private class syncSuppressionTest {
}

However, I'm just pointing out what's happening from a FYI standpoint... It's not good practice to set the SeeAllData flag to true, test classes should be as self sufficient as possible, so that means creating you're own test data.

 

So to improve your test class, instead of hardcoding the Id's of records insert new records for what you need. Since I don't know the complete data model this is just a rough look at what you need to do:

 

@isTest
private class syncSuppressionTest {
	
	static testMethod void validateSyncSuppression(){
		Account a = new Account(Name='Main Client Test', Website='www.dummywebsite.com');
		insert a;
		
		Contact c = new Contact(LastName='Contact', FirstName='Test', AccountId=a.Id);
		insert c;
		
		Campaign campaign = new Campaign(Name = 'Test Campaign', Client_Account__c = a.Id);
		insert campaign;
		
		// new opportunity
		Opportunity o = new Opportunity(
		Name='testsync', 
		StageName='0 BANT',
		Type='New Client QSO Opportunity',
		AccountId = a.Id,
		CloseDate= date.today(),
		CampaignID= campaign.Id,
		Main_Contact__c= c.Id
		);
		System.debug('new op made: ' + o.name);
		
		//insert op
		insert o;
		
		//update op
		o.StageName='Submitted';
		update o;
		System.debug('new op updated: ' + o.StageName);		
	}	
}

 

 

Ryan-HaireRyan-Haire

This worked perfectly. Thank you for all the help and explanations. This was a great learning experience and I definitely couldn't have got this to work without your help.

 

 

Best,

Ryan

Ryan-HaireRyan-Haire

So close-- I tried to deploy the trigger today and came up with an error while validating the inbound changeset.

 

Any idea what could cause this or a way to add it to the code/test class to test?

 

Failure Message: "System.AssertException: Assertion Failed: Expected: 2, Actual: 1", Failure Stack Trace: "Class.trac_OpportunityClone_Test.theTest: line 81, column 1"

 

 

Sean TanSean Tan

You'll have to paste the code for the class:

 

trac_OpportunityClone_Test

 

I don't think it has anything in particular to do with your test class (unless you named it that with assert statements?). It could relate to some other expected behaviour that already exists on the org.

Ryan-HaireRyan-Haire

Finally fixed it-- It had nothing to do with anything we've been working on actually-- There was a broken trigger in the org and I was actually able to track down the problem, fix the code and fix the trigger-- so that was pretty cool. Thanks again for all of the help. This was a great entry into some basic coding.

 

Best,

Ryan