Automatically roll up Salesforce enhanced notes to the relevant account

Salesforce’s Note object is being superseded by its ContentNote object. A limitation of the newer notes is that they don’t automatically roll up under the associated account when added to contacts and opportunities.

Here is some code I’ve written to solve this (also includes case notes):

Apex Trigger

trigger ContentDocumentLinkTrigger on ContentDocumentLink (after insert) {
    Set<ID> ids = Trigger.newMap.keySet();
    List <ContentDocumentLink> docLinks = [SELECT LinkedEntityId,ContentDocumentId,ContentDocument.FileType 
                                           FROM ContentDocumentLink WHERE Id IN: ids];
    for (ContentDocumentLink docLink : docLinks) {
        if (docLink.ContentDocument.FileType == 'SNOTE'){
            String linkedId = docLink.LinkedEntityId;
            if(!linkedId.startsWith('001') && !linkedId.startsWith('005')){
                String sNoteId = docLink.ContentDocumentId;
                NoteLinkHelper.linkToAcct(sNoteId,linkedId);
            }
        }
    }
}

Apex Class

public class NoteLinkHelper {
    
    //helper method to get account ID related to non-account record from which note was created
    public static String getLinkedEntityAccount(String noteId, String linkedId) {
        String acctId;
        if (linkedId.startsWith('003')){
            List<Contact> contacts = [SELECT AccountId FROM Contact WHERE Id =: linkedId];
            if (!contacts.isEmpty()){
                if (contacts[0].AccountId != null) {
                    acctId = contacts[0].AccountId;
                }
            }
        }
        else if (linkedId.startsWith('006')){
            List<Opportunity> opps = [SELECT AccountId FROM Opportunity WHERE Id =: linkedId];
            if (!opps.isEmpty()){
                if (opps[0].AccountId != null) {
                    acctId = opps[0].AccountId;
                }
            }
        }
        else if (linkedId.startsWith('500')){
            List<Case> cases = [SELECT AccountId FROM Case WHERE Id =: linkedId];
            if (!cases.isEmpty()){
                if (cases[0].AccountId != null) {
                    acctId = cases[0].AccountId;
                }
            }
        }
        return acctId;
    }
    
    //helper method to link note to an account
    public static void linkToAcct(String sNoteId,String linkedId) {
        String acctToLink = getLinkedEntityAccount(sNoteId,linkedId);
        if (acctToLink != null) {
            ContentDocumentLink sNoteLink = new ContentDocumentLink();
            sNoteLink.ContentDocumentId = sNoteId;
            sNoteLink.LinkedEntityId = acctToLink;
            sNoteLink.ShareType = 'V';
            insert(sNoteLink);
        }
    }
}

I hope this helps your users get a more comprehensive view of their accounts.

Advertisements

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

An employee contact record for every Salesforce user

There are a number of reasons why it might be helpful for every Salesforce user to have a related contact record that is updated whenever certain user fields are updated. A couple of those reasons might be:

  • using cases for internal support (to benefit from case contact functionality)
  • making the built-in contact hierarchy org chart work for your company

Here is an Apex class I’ve created to accomplish this:

public with sharing class UpsertUserContact {
    @Future
    public static void execute(Set<Id> userIds) {

        List<Contact> contactsToUpsert = new List<Contact>(); // Create a list of contacts to upsert

        List<RecordType> employeeRTs = [SELECT Id FROM RecordType
                                        WHERE SobjectType = 'Contact' AND DeveloperName = 'Employee'
                                        LIMIT 1];

        List <Employee_Contact_Setting__mdt> companyAcctIds = [SELECT ID__c
                                                               FROM Employee_Contact_Setting__mdt
                                                               WHERE DeveloperName =: 'Company_Account'
                                                               LIMIT 1];

        List<User> users =
            [SELECT Id, Email, FirstName, LastName, Department, ManagerId, Fax, Phone, MobilePhone, Title, Street, City, State, PostalCode, Country
             FROM User
             WHERE Id IN : userIds AND IsActive = true AND UserType = 'Standard'];

        List<Id> userManagerIds = new List<Id>();

        for(User managedUser: users){
            if (managedUser.ManagerId != null){
                userManagerIds.add(managedUser.ManagerId);
            }
        }

        List<Contact> managerContacts = [SELECT Id,User__c from Contact
                                         WHERE User__c IN : userManagerIds];

        Map <Id,Id> managerContactMap = new Map<Id,Id>();	// Create a manager contact map of userId, contactId

        for(Contact managerContact: managerContacts){
            managerContactMap.put(managerContact.User__c,managerContact.Id);
        }

        List<Contact> contacts =
            [SELECT Id, User__c
             FROM Contact
             WHERE User__c IN: userIds];

        Map <Id,Id> userContactMap = new Map <Id,Id>();	// Create a user contact map of userId, contactId

        for(Contact userContact: contacts){
            userContactMap.put(userContact.User__c,userContact.Id);
        }

        for (User u : users){								// Loop through each upserted user

            Contact c = new Contact(						// Create a contact record in memory
                RecordTypeId = employeeRTs[0].Id,			// Populate the record type
                User__c = u.Id,								// Populate the user lookup
                Email = u.Email,							// Populate the email
                FirstName = u.FirstName,					// Populate the first name
                LastName = u.LastName,						// Populate the last name
                Department = u.Department,					// Populate the user department
                Fax = u.Fax,								// Populate the fax number
                Phone = u.Phone,							// Populate the phone number
                MobilePhone = u.MobilePhone,				// Populate the mobile phone number
                Title = u.Title,							// Populate the title of user
                MailingStreet = u.Street,					// Populate the mailing street
                MailingCity = u.City,						// Populate the mailing city
                MailingState = u.State,						// Populate the mailing state
                MailingPostalCode = u.PostalCode,			// Populate the postal code
                MailingCountry = u.Country,					// Populate the country
                OwnerId = u.Id);							// Populate the contact owner
            if (companyAcctIds.size() > 0) {
                c.AccountId = companyAcctIds[0].ID__c;		// Populate the account lookup
            }
            if (managerContactMap.get(u.ManagerId) != null) {
                c.ReportsToId = managerContactMap.get(u.ManagerId); // Populate the Reports To field
            }
            if (userContactMap.get(u.Id) != null) {
                c.Id = userContactMap.get(u.Id);			// specify the contact to be updated
            }

            contactsToUpsert.add(c);						// Add the contact to the bulk upsert list

        }

        if(contactsToUpsert.size() > 0){
            upsert contactsToUpsert;						// Upsert all contacts in single DML statement
        }
    }

}

Here’s the Apex trigger I created to call the above class:

trigger UserTrigger on User (after delete, after insert, after undelete, after update, before delete, before insert, before update) {

    //Handles all user triggers

    if ((trigger.isInsert || trigger.isUpdate) && trigger.isAfter){

            Set<Id> userIds = new Set<Id>();

            for (User u : trigger.new) {

                // Add the user id to the set of ids
                userIds.add(u.Id);
            }
            if (!System.isFuture() && !System.isBatch()) {
                //upsert contact records with the changes in user records
                UpsertUserContact.execute(userIds);
            }

    }
}

Prerequisites:

  • custom lookup field called User (User__c) on the contact object
  • custom metadata type of Employee Contact Setting (Employee_Contact_Setting__mdt) with
    • custom field of ID (ID__c)
    • record called Company Account (Company_Account)

There you have it!