Opportunities are a key object in the sales cycle when you’re using Salesforce as your CRM. It would make sense that you should be able to require your sales reps to relate a contact record to an opportunity at some point, wouldn’t it? Some connected apps, such as Marketo, rely on an opportunity having a contact with an email address. Unfortunately, addressing these requirements isn’t as easy as it ideally should be.
Contact records are related to an opportunity by way of a junction object known as Opportunity Contact Role. That creates the option to have many contacts related to an opportunity but prevents the option to accomplish the desire here by simply making a field required. Also, Opportunity Contact Role isn’t a first-class object, which makes it unavailable for standard roll-up summaries or Declarative Lookup Rollup Summaries.
What’s a dev to do with all of these challenges? There’s an Opportunity Contact Roles Validation app but it’s not bulkified, doesn’t account for deletes in its calculations, and can only be installed in sandboxes. I found a free Opportunity Primary Contact Required app from Salesforce Labs, customized it, and added some auto-add Opportunity Contact Role logic inspired by my colleague Tena Wolver. Below are the results.
Contact Required? (contact_required__c) – formula field checkbox on the Opportunity object:
OR( ISPICKVAL(StageName,'Proposal/Price Quote'), ISPICKVAL(StageName,'Negotiation/Review') )
OpportunityContactRoleTrigger – trigger on the Opportunity object:
trigger OpportunityContactRoleTrigger on Opportunity (after insert, before update) { //if an account's opp is created or updated and doesn't have a contact role, add account's most recently edited contact with an email address if ((Trigger.isInsert && Trigger.isAfter) || (Trigger.isUpdate && Trigger.isBefore)) { //get the list of accountids from opportunity Map<Opportunity,ID> OppToAcctMap = new Map<Opportunity,ID>(); for (Opportunity opp : Trigger.new) { if (opp.AccountId != null) { OppToAcctMap.put(opp,opp.AccountID); } } //get the account and its eligible contacts Map<ID,Account> accContacts = new Map<ID,Account>([SELECT Id, (SELECT Id FROM contacts WHERE Email != null AND HasOptedOutOfEmail = false ORDER BY LastModifiedDate DESC NULLS LAST) from Account WHERE Id IN: OppToAcctMap.values()]); //get opp contact roles if any Map<ID,Opportunity> oppMap = new Map<ID,Opportunity>([SELECT Id,AccountId, (SELECT Id FROM OpportunityContactRoles) FROM Opportunity WHERE Id IN: Trigger.new]); //insert contact role if we can List<OpportunityContactRole> ocrList = new List<OpportunityContactRole>(); for (Opportunity oppMapValue : oppMap.values()){ if (oppMap.get(oppMapValue.Id).OpportunityContactRoles.isEmpty() && oppMapValue.AccountId != null){ if (accContacts.containsKey(oppMapValue.AccountId) && !accContacts.get(oppMapValue.AccountId).contacts.isEmpty()) { OpportunityContactRole ocr = new OpportunityContactRole(); ocr.ContactID = accContacts.get(oppMapValue.AccountId).contacts.get(0).Id; ocr.OpportunityId = oppMapValue.id; ocr.IsPrimary = true; ocrList.add(ocr); } } } insert ocrList; } // end Auto-add Contact Role //if an opportunity's Contact Required? checkbox is checked, require a primary contact role with an email address if (Trigger.isUpdate && Trigger.isBefore) { //map to keep track of the contact_required = true Map<String, Opportunity> oppy_contact = new Map<String, Opportunity>(); //adds any updated opps that have Contact_Required = true to the oppy_contact Map for (Integer i = 0; i < Trigger.new.size(); i++) { if (Trigger.new[i].contact_required__c) { oppy_contact.put(Trigger.new[i].id,Trigger.new[i]); } } //map to keep track of the opportunity contact roles Map<Id, OpportunityContactRole> primaryoppycontactroles = new map<Id, OpportunityContactRole>(); Map<Id, OpportunityContactRole> primaryoppycontactroleswithemail = new map<Id, OpportunityContactRole>(); //select OpportunityContactRoles for the opportunities with contact role required List<OpportunityContactRole> roles = [SELECT OpportunityId, IsPrimary FROM OpportunityContactRole WHERE IsPrimary = true AND OpportunityId IN: oppy_contact.keySet()]; List<OpportunityContactRole> roleswithemail = [SELECT OpportunityId, IsPrimary FROM OpportunityContactRole WHERE IsPrimary = true AND OpportunityId IN: oppy_contact.keySet() AND Contact.Email != null]; for (OpportunityContactRole ocr : roles) { //puts the contact roles in the map with the Opportunity ID as the key primaryoppycontactroles.put(ocr.OpportunityId,ocr); } for (OpportunityContactRole ocr : roleswithemail) { //puts the contact roles in the map with the Opportunity ID as the key primaryoppycontactroleswithemail.put(ocr.OpportunityId,ocr); } // Loop through the opportunities where contact role is required for (Opportunity oppy : oppy_contact.values()) { if (!primaryoppycontactroles.containsKey(oppy.Id)) { oppy.addError('No Primary Contact Exists. Please go to the Contact Roles related list and select or add a primary contact.'); } if (primaryoppycontactroles.containsKey(oppy.Id) && !primaryoppycontactroleswithemail.containsKey(oppy.Id)) { oppy.addError('Primary Contact Does Not Have Email Address. Please add an email address to the primary contact.'); } } //end for loop } //end Require Primary Contact Role with Email Address } //end trigger
OpportunityContactRoleTriggerTest – Apex test class:
@isTest private class OpportunityContactRoleTriggerTest { // test to ensure an opportunity can be added and have contact role auto-added public static testMethod void testoppyautoaddcontactrole() { //add an opportunity without a contact, and with an account that has a contact with email address Account acct = new Account(Name='test account'); insert acct; Contact cont = new Contact(LastName='testLastName',Email='testcontact@example.com',AccountId=acct.Id); insert cont; //Opportunity oppy = new Opportunity(Name='nick_test',StageName='Perception Analysis',CloseDate=System.Today(),AccountId=acct.Id); List<Opportunity> oppy = new List<Opportunity>(); for (Integer i = 0; i < 10; i++) { oppy.add(new Opportunity(Name='testopp'+i,StageName='Perception Analysis',CloseDate=System.Today(),AccountId=acct.Id)); } insert oppy; System.assert([SELECT count() FROM OpportunityContactRole WHERE OpportunityId =: oppy[4].Id] == 1); } //end testoppyautoaddcontactrole // test to ensure an opportunity can be created without a contact role public static testMethod void testoppyrequiredfalse() { //create oppty List<Opportunity> oppy = new List<Opportunity>(); //add 10 opportunites without a contact, and with the condition contact required = 0 for (Integer i = 0; i < 10; i++) { oppy.add(new Opportunity(Name='testopp'+i,StageName='Perception Analysis',CloseDate=System.Today())); } insert oppy; map<Id, Opportunity> oppy_map = new map<Id, Opportunity>(); for (Integer i = 0;i<10;++i){ oppy_map.put(oppy[i].Id,oppy[i]); } //for System.assert([SELECT count() FROM Opportunity WHERE Id IN :oppy_map.keySet()] == 10); } //end testoppyrequired = false //test to go from a not required value to a required value public static testMethod void testoppyrequiredtrue() { //create oppty List<Opportunity> oppy2 = new List<Opportunity>(); //add 10 opportunites without a contact, and with the condition contact required = 0 for (Integer i = 0; i < 10; i++) { oppy2.add(new Opportunity(Name='testopp'+i,StageName='Qualification',CloseDate=System.Today())); } insert oppy2; for (Integer i = 0; i < 10; i++) { oppy2[i].StageName='Negotiation/Review'; } Test.startTest(); try { update oppy2; Opportunity sampleTest = [Select Id, Contact_Required__c From Opportunity where Id = :oppy2[0].id]; System.debug('***** SAMPLE' + sampleTest); System.assert(false, 'This update should have failed.'); } catch(System.DmlException e) { System.assert(e.getMessage().contains('No Primary Contact Exists.')); } Test.stopTest(); } //end testoppyrequired = true public static testMethod void testoppyrequiredtruewoprimary() { List<Opportunity> oppy = new List<Opportunity>(); //add 10 opportunites for (Integer i = 0; i < 10; i++) { oppy.add(new Opportunity(Name='testopp'+i,StageName='Qualification',CloseDate=System.Today())); } insert oppy; //add 10 contacts List<Contact> c = new List<Contact>(); for (Integer i = 0; i < 10; i++) { c.add(new Contact(LastName='testLastName'+i,Email='testcontact'+i+'@example.com')); } insert c; for (Integer i = 0; i < 10; i++) { oppy[i].StageName='Negotiation/Review'; } //add 10 opporunity contact roles associated to the opportunities and contacts above List<OpportunityContactRole> ocr = new List<OpportunityContactRole>(); for (Integer i = 0; i < 10; i++) { ocr.add(new OpportunityContactRole(Role='Business User',OpportunityId=oppy[i].id,ContactId=c[i].id)); } insert ocr; boolean caughtException = false; Test.startTest(); try { update oppy; } catch(System.DmlException e) { System.assert(e.getMessage().contains('No Primary Contact Exists.')); caughtException = true; } Test.stopTest(); System.assert(caughtException); } //end testoppyrequired = true public static testMethod void testoppyrequiredtrueprimary() { //create oppty list List<Opportunity> oppy = new List<Opportunity>(); //add 10 opportunites for (Integer i = 0; i < 10; i++) { oppy.add(new Opportunity(Name='testopp'+i,StageName='Qualification',CloseDate=System.Today())); } insert oppy; Map<Id, Opportunity> oppy_map = new Map<Id, Opportunity>(); for (Integer i = 0;i<10;++i){ oppy_map.put(oppy[i].Id,oppy[i]); } //end for loop //add 10 contacts List<Contact> c = new List<Contact>(); for (Integer i = 0; i < 5; i++) { c.add(new Contact(LastName='testLastName'+i,Email='testcontact'+i+'@example.com')); } for (Integer i = 0; i < 5; i++) { c.add(new Contact(LastName='testingLastName'+i)); } insert c; //add 10 opporunity contact roles associated to the opportunities and contacts above List<OpportunityContactRole> ocr = new List<OpportunityContactRole>(); for (Integer i = 0; i < 10; i++) { ocr.add(new OpportunityContactRole(Role='Business User',OpportunityId=oppy[i].id,ContactId=c[i].id,IsPrimary=True)); } insert ocr; for (Integer i = 0; i < 10; i++) { oppy[i].StageName='Negotiation/Review'; } try { update oppy; System.assert([SELECT count() FROM Opportunity WHERE Id IN :oppy_map.keySet()] == 10); } catch(System.DmlException e) { System.assert(true); } } //end testoppyrequired = true and primary contact = true } //end test class
Happy contacting! 🙂
Hi Rob,
I think this solution will work perfectly for us, but I need a little help with coding. Thanks for sharing. I am not a coder by any means. The one part I’m not sure about is the Primary Contact. We want to require at least one contact role to be required but they don’t have to be marked as primary. Would be easy to change the code?
Second question: if we want the required contact role to include the text “Buyer”, is is possible to be that specific on the rule? e.g. Required contact role must include “buyer”.
Thank you!
Ember
LikeLike