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!

Making Salesforce’s Live Agent Button & Deployment Code Portable

During a recent implementation of Salesforce’s Live Agent website chat product, I decided to see how portable I could make the button & deployment code, to minimize the environment-specific changes that would have to be made.

Here is what I came up with:


<html>
   <body>
      <!-- Begin Live Agent button code -->
         <a id="onlineContent" href="javascript://Chat">
            <!-- Online Chat Content -->
         </a>
<div id="offlineContent">
            <!-- Offline Chat Content -->
</div>
<script type="text/javascript">
            var buttonId;
            function beginChat(){
               liveagent.startChat(buttonId);
            }
            if (!window._laq) { window._laq = []; }
            window._laq.push(function(){
               document.getElementById('onlineContent').onclick = beginChat;
               liveagent.showWhenOnline(buttonId, document.getElementById('onlineContent'));
               liveagent.showWhenOffline(buttonId, document.getElementById('offlineContent'));
            });
         </script>
      <!-- End Live Agent button code -->

      <!-- Begin Live Agent deployment code -->
         <script type='text/javascript' src = 'https://la1c1.salesforceliveagent.com/content/g/js/39.0/deployment.js'>
         </script>
         <script type='text/javascript'>
            var firstName;
            var emailAddress;
            var endpointUrl;
            var deploymentId;
            var orgId;

            if (firstName != null) {
               // Sets the display name of the visitor when engaged in a chat.
               liveagent.setName(firstName);
            }
            if (emailAddress != null) {
               // Creates a custom detail called Email and sets its value.
               liveagent.addCustomDetail('Email', emailAddress);
               // Searches for a contact with an exact match to the Email custom detail. Creates a new contact if needed. Links contact to case.
               liveagent.findOrCreate('Contact').map('Email','Email',true,true,true).saveToTranscript('ContactId').linkToEntity('Case','ContactId');
            }
               // Creates a custom detail called Case Origin and sets its value to Chat.
               liveagent.addCustomDetail('Case Origin', 'Chat');
               // Creates a case and sets its origin. Associates the case to the chat transcript and opens the case in the agent console.
               liveagent.findOrCreate('Case').map('Origin','Case Origin',false,false,true).saveToTranscript('CaseId').showOnCreate();
               // Initiates the chat
               liveagent.init(endpointUrl, deploymentId, orgId);
         </script>
      <!-- End Live Agent deployment code -->
   </body>
</html>

This code should work for any Live Agent implementation. Simply do the following:

  • modify the online and offline content elements as desired
  • add the endpointUrl, deploymentId, and orgId from Salesforce
  • populate the firstName and emailAddress variables via your website

Enjoy!  🙂

Using Salesforce’s SandboxPostCopy interface to modify email addresses

In Salesforce’s Spring ’16 release, the Run Script After Sandbox Creation and Refresh feature was released. It allows developers to create an Apex class that implements the SandboxPostCopy interface and specify the class on the Sandbox Options screen (pictured in the screenshot at the top of this post).

One use for this is to modify email addresses that are included in records as part of refreshing a partial copy or full sandbox. Salesforce automatically modifies sandbox user email addresses to the format of emailname=domain.com@example.com but doesn’t do so for other types of records (such as leads, contacts, or queues). Leaving these unmodified could end up in undesired email messages being sent to real prospects, customers, or employees.

Recently on GitHub, I found some code published by Steve O’Neal that uses SandboxPostCopy in combination with the Batchable interface to modify sandbox email addresses in a way that can handle large numbers of records. I made a few modifications to Steve’s code, including:

  • changing the batch classes so the test class will work in production orgs (not just sandboxes)
  • adding the group object (type = ‘queue’)
  • making the modified contact/lead/queue email addresses to be in the same format as the user email addresses

I created a sandbox-postcopy repository in GitHub with the updated classes and their XML files. Feel free to fork that repository and/or deploy it to your Salesforce org.