Requiring a Related Contact Record on Salesforce Opportunities

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!  🙂

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s