Calculate amount of specified task records which are related to Contact and Account in Salesforce

Pretty often you need to know how many times some Contact has been used in Log a Call functionality. Aggregation of such count in an account level might be pretty useful too. So, let's implement this functionality. I faced with such requirement a few times with different customers, so I'd like to share my experience.

All you know, in the perfect world we should use built-in functionality and avoid a custom implementations. So, firstly I checked for a way to build it with some Salesforce native functionality. First thing which was considered is a Roll-up summary field. But we can't use Roll-up summary fields here, because these object has no master-detail relationship by default. Sure, you can create such relation, but it's no a case for me. The result of quick investigation lets me think that trigger will be an ideal solution for such functionality.

So, firstly I create 2 fields Reference_Count__c on Contact and Account. Both fields has Number type. After that I create a trigger on Task object which runs after insert, after update and after delete.

There are the following cases:

  • When user creates task with Type = 'Call' we have to increment counter on Contact which is related to this task and also we have to increate a count on Account which is related to Contact.
  • When user changes type of task from any to 'Call' we have to increment counters.
  • When user changes type of task from 'Call' to any other we have to decrement counter for Contact and Account related to this task.
  • Also we have to decrement counters when user removes Task

You can find a whole solution on GitHub.

Let's break each point to separate method:

  1. When user creates a task with Type = 'Call' we have to increment counter on Contact which is related to this task and we also have to increment a count on Account which is parent to Contact.
private static void increaseReferenceCountOnInsert(List<sObject> newRecords) {
    List<sObject> taskRelatedToContact = new List<sObject>();
    for (SObject obj : newRecords) {
        if (((String)obj.get('WhoId')).startsWith(CONTACT_RECORDS_PREFIX) && ((String)obj.get('Type')) == LOG_A_CALL_TYPE) {
            taskRelatedToContact.add(obj);
        }
    }
    updateReferenceCountField(taskRelatedToContact, true);
}
  1. When user changes type of task from any to 'Call' we have to increase counters.
private static void increaseReferenceCountOnUpdate(List<sObject> newRecords, Map<ID, sObject> oldRecordsMap) {
    List<sObject> taskRelatedToContact = new List<sObject>();
    for (SObject obj : newRecords) {
        if (((String)obj.get('WhoId')).startsWith(CONTACT_RECORDS_PREFIX)
            && ((String)obj.get('Type')) == LOG_A_CALL_TYPE
            && ((String)(oldRecordsMap.get((Id)obj.get('Id'))).get('Type')) != LOG_A_CALL_TYPE) {
            taskRelatedToContact.add(obj);
        }
    }
    updateReferenceCountField(taskRelatedToContact, true);
}

The next method is used in 2 cases: 3) When user changes type of task from 'Call' to any other we have to decrease counter for Contact and Account to which this task is related. 4) We have to decrease counters when user removes the Task

private static void decreaseReferenceCountOnTaskTypeChange(List<sObject> newRecords, Map<ID, sObject> oldRecordsMap) {
    List<sObject> taskToDecreaseCount = new List<sObject>();
    for (SObject obj : newRecords) {
        if (Trigger.isUpdate) {
            if (((String)obj.get('WhoId')).startsWith(CONTACT_RECORDS_PREFIX)
                && ((String)obj.get('Type')) != LOG_A_CALL_TYPE
                && ((String)(oldRecordsMap.get((Id)obj.get('Id'))).get('Type')) == LOG_A_CALL_TYPE) {
                taskToDecreaseCount.add(obj);
            }
        } else if (Trigger.isDelete && ((String)obj.get('Type')) == LOG_A_CALL_TYPE ) {
            taskToDecreaseCount.add(obj);
        }
    }
    updateReferenceCountField(taskToDecreaseCount, false);
}

You can find that all these methods invoke updateReferenceCountField method

private static void updateReferenceCountField(List<sObject> tasks, Boolean isIncrement) {
    Map<Id, List<sObject> tasksByContact = splitListBySpecialKey(tasks, 'WhoId');
    List<Contact> relatedContactsForUpdateCountField = [
        SELECT Id, Reference_Count__c, AccountId
        FROM Contact
        WHERE Id IN: tasksByContact.keySet()
    ];
    updateAccounts(relatedContactsForUpdateCountField, tasksByContact, isIncrement);
        updateContacts(relatedContactsForUpdateCountField, tasksByContact, isIncrement);
 }

Which invokes 2 methods which actually perform DML

private static void updateAccounts(List<Contact> relatedContactsForUpdateCountField,
                                   Map<Id, List<sObject>> tasksByContact, Boolean isIncrement) {
    // ContactId, Account
    Map<Id,Id> contactIdByAccountId = new Map<Id,Id>();
    for (Contact cont: relatedContactsForUpdateCountField) {
        contactIdByAccountId.put(cont.AccountId, cont.Id);
    }
    List<Account> accountsRelatedToContactsForUpdate = [
        SELECT Id, Reference_Count__c
        FROM Account
        WHERE Id IN: contactIdByAccountId.keySet()
    ];

    for (Account acc: accountsRelatedToContactsForUpdate) {
        if (acc.Reference_Count__c != null) {
            if (isIncrement) {
                acc.Reference_Count__c += tasksByContact.get(contactIdByAccountId.get(acc.Id)).size();
            } else {
                acc.Reference_Count__c -= tasksByContact.get(contactIdByAccountId.get(acc.Id)).size();
            }
        } else {
            acc.Reference_Count__c = tasksByContact.get(contactIdByAccountId.get(acc.Id)).size();
        }
    }
    update accountsRelatedToContactsForUpdate;
}

private static void updateContacts(List<Contact> relatedContactsForUpdateCountField,
                                    Map<Id, List<sObject> tasksByContact, Boolean isIncrement) {
    for (Contact cont: relatedContactsForUpdateCountField) {
        Integer amountOfNewTasks = tasksByContact.get(cont.Id).size();
        if (amountOfNewTasks > 0) {
            if (cont.Reference_Count__c != null) {
                if (isIncrement) {
                    cont.Reference_Count__c += amountOfNewTasks;
                } else {
                    cont.Reference_Count__c -= amountOfNewTasks;
                }
            } else {
                cont.Reference_Count__c = amountOfNewTasks;
            }
        }
    }
    update relatedContactsForUpdateCountField;
}

We also need this utility method for transformation list into map

public static Map<Id, List<sObject> splitListBySpecialKey(List<sObject> sourceList, String key) {
    if (sourceList == null) {
        throw new IncorrectParameterException('ERROR: splitListBySpecialKey(sourceList, key) got incorrect first parameter.');
    }
    if (String.isBlank(key)) {
        throw new IncorrectParameterException('ERROR: splitListBySpecialKey(sourceList, key) got incorrect second parameter.');
    }
    Map<Id, List<sObject> result = new Map<Id, List<sObject>();
    List<sObject> tmpObjs;
    for (sObject obj : sourceList) {
        tmpObjs = new List<sObject>();
        if (obj.get(key) != null && result.containsKey((Id)obj.get(key))) {
            tmpObjs = result.get((Id)obj.get(key));
            tmpObjs.add(obj);
            result.put((Id)obj.get(key), tmpObjs);
        } else if (obj.get(key) != null) {
            tmpObjs.add(obj);
            result.put((Id)obj.get(key), tmpObjs);
        }
    }
    return result;
 }

public class IncorrectParameterException extends System.Exception {}