Skip to content

Instantly share code, notes, and snippets.

@eskfung
Last active August 11, 2022 03:16
Show Gist options
  • Save eskfung/f342b47ecc0849deb0cb to your computer and use it in GitHub Desktop.
Save eskfung/f342b47ecc0849deb0cb to your computer and use it in GitHub Desktop.
Apex for round robin assignments of Salesforce leads and cases.

Setup Instructions

  • Create a Salesforce Queue that can be assigned to Cases and/or Leads. Setup -> Administration -> Users -> Queues
  • Create a new Assignment Group. This will help match Salesforce Queue(s) to the Users you want to assign via round robin.
  • Create an Assignment Group Queue that is related to the previous Assignment Group. The queue name must match a Salesforce Queue name. A queue also cannot be used in two different Assignment Groups. There is no need to assign Users or Roles here yet.
  • Add Assignment Group Members to the Assignment Group. This is where you link a potential Lead/Case owner to the Assignment Group.

Schema

Assignment Group

  • Assigment Group Name: Text(80)
  • Description: Long Text Area(131072)
  • User Count: Roll-Up Summary (COUNT Assignment Group Member)

Assignment Group Queue

  • Assignment Group Queue Name: Text(80)
  • Active: Checkbox
  • Assignment Group: Master-Detail(Assignment Group)
  • QueueID: Text(18)

Assignment Group Member

  • Assignment Group Member Number: Auto Number
  • Active: Checkbox
  • Assignment Group: Master-Detail(Assignment Group)
  • Last Assigned: Date/Time
  • Last Assigned Milliseconds: Number(3, 0)
  • User: Lookup(User)
  • User Available?: Formula (Checkbox) User__r.IsActive && Active__c
trigger ApplyAssignmentGroupsToCase on Case (after update) {
Map<Id, Id> caseOwners = new Map<Id, Id>();
for (Case c : Trigger.new) {
if (Trigger.isUpdate) {
if (c.OwnerId != Trigger.oldMap.get(c.id).OwnerId) {
caseOwners.put(c.Id, c.OwnerId);
}
} else {
caseOwners.put(c.Id, c.OwnerId);
}
}
AssignmentGroupAssistant assistant = new AssignmentGroupAssistant(caseOwners);
Map<Id, Id> newAssignments = assistant.newOwnerAssignments();
List<Case> casesToUpdate = new List<Case>([
SELECT Id,OwnerId FROM Case WHERE Id in :newAssignments.keySet()]
);
for (Case c : casesToUpdate) {
c.OwnerId = newAssignments.get(c.Id);
}
try {
if (casesToUpdate.size() > 0) {
update casesToUpdate;
update assistant.groupMembersToUpdate();
}
} catch (DMLException e) {
System.debug('The following exception has occurred: ' + e.getMessage());
}
}
trigger ApplyAssignmentGroupsToLead on Lead (after update) {
Map<Id, Id> leadOwners = new Map<Id, Id>();
for (Lead l : Trigger.new) {
if (Trigger.isUpdate) {
if (l.OwnerId != Trigger.oldMap.get(l.id).OwnerId) {
leadOwners.put(l.Id, l.OwnerId);
}
} else {
leadOwners.put(l.Id, l.OwnerId);
}
}
AssignmentGroupAssistant assistant = new AssignmentGroupAssistant(leadOwners);
Map<Id, Id> newAssignments = assistant.newOwnerAssignments();
List<Lead> leadsToUpdate = new List<Lead>([
SELECT Id,OwnerId FROM Lead WHERE Id in :newAssignments.keySet()]
);
for (Lead l : leadsToUpdate) {
l.OwnerId = newAssignments.get(l.Id);
}
try {
if (leadsToUpdate.size() > 0) {
update leadsToUpdate;
update assistant.groupMembersToUpdate();
}
} catch (DMLException e) {
System.debug('The following exception has occurred: ' + e.getMessage());
}
}
public with sharing class AssignmentGroupAssistant {
Map<Id, Id> sobjectToOwnerMap; // Sobject ID -> Owner ID
Map<Id, Id> ownerToAssgnGroupsMap; // Owner ID -> Assignment Group ID - via Assignment Group Queue
Map<Id, Id> sobjectToAssgnGroupMap; // Sobject ID -> Assignment Group ID
Map<Id, List<Id>> assgnGroupToSobjectMap; // Assignment Group ID -> Sobject IDs[]
Map<Id, List<Assignment_Group_Member__c>> assgnGroupsToMembersMap; // Assignment Group ID -> Assignment Group Members[]
Map<Id, Assignment_Group_Member__c> membersToUpdate;
public AssignmentGroupAssistant(Map<Id, Id> sobjectOwners) {
membersToUpdate = new Map<Id, Assignment_Group_Member__c>();
sobjectToOwnerMap = sobjectOwners;
}
// @return Map{Sobject ID -> User ID}
public Map<Id, Id> newOwnerAssignments() {
Map<Id, Id> ownerAssignments = new Map<Id, Id>();
for (Id assgnGroupId : assignmentGroupsToSobjects().keyset()) {
List<Id> sobjectsToAssign = assignmentGroupsToSobjects().get(assgnGroupId);
List<Assignment_Group_Member__c> groupMembers = assgnGroupsToMembers().get(assgnGroupId);
Integer index = 0;
for (Id lId : sobjectsToAssign) {
// Pick the next person to be assigned
Assignment_Group_Member__c member = groupMembers.get(Math.mod(index, groupMembers.size()));
ownerAssignments.put(lId, member.User__c);
logLastAssignment(member);
index++;
}
}
return ownerAssignments;
}
// @return Group members with new last-assigned dates
public List<Assignment_Group_Member__c> groupMembersToUpdate() {
return membersToUpdate.values();
}
// If Assignment_Group_Member__c was used in assignment, change its assignment date and queue for updating
@TestVisible private void logLastAssignment(Assignment_Group_Member__c member) {
// Log last assignment datetime
DateTime now = DateTime.now();
member.Last_Assigned__c = now;
member.Last_Assigned_Milliseconds__c = now.millisecondGMT();
Assignment_Group_Member__c tempMember = membersToUpdate.get(member.Id);
if (tempMember != null) {
if (tempMember.Last_Assigned__c < member.Last_Assigned__c ||
(tempMember.Last_Assigned__c == member.Last_Assigned__c &&
tempMember.Last_Assigned_Milliseconds__c < member.Last_Assigned_Milliseconds__c)
) { // This Group Member has a later assignment
membersToUpdate.put(member.Id, member);
}
} else {
membersToUpdate.put(member.Id, member);
}
}
// @return Map{Assignment Group ID -> List<Assignment Group Members>}
@TestVisible private Map<Id, List<Assignment_Group_Member__c>> assgnGroupsToMembers() {
if (assgnGroupsToMembersMap == null) {
assgnGroupsToMembersMap = new Map<Id, List<Assignment_Group_Member__c>>();
for (Assignment_Group_Member__c member : [SELECT Assignment_Group__c, User__c, Last_Assigned__c, Last_Assigned_Milliseconds__c
FROM Assignment_Group_Member__c
WHERE Assignment_Group__c in :assignmentGroupsToSobjects().keyset()
AND User_Available__c = True
ORDER BY Last_Assigned__c, Last_Assigned_Milliseconds__c]) {
List<Assignment_Group_Member__c> memberList = assgnGroupsToMembersMap.get(member.Assignment_Group__c);
if (memberList == null) {
memberList = new List<Assignment_Group_Member__c>();
}
memberList.add(member);
assgnGroupsToMembersMap.put(member.Assignment_Group__c, memberList);
}
}
return assgnGroupsToMembersMap;
}
// Filters the list of sobjects to those who are being reassigned
// @return Map{Assignment Group ID -> Sobject IDs[]}
@TestVisible private Map<Id, List<Id>> assignmentGroupsToSobjects() {
if (assgnGroupToSobjectMap == null) {
assgnGroupToSobjectMap = new Map<Id, List<Id>>();
for (Id sobjectId : sobjectToOwnerMap.keySet()) {
Id assgGroupId = ownersToAssignmentGroups().get( sobjectToOwnerMap.get(sobjectId) );
if (assgGroupId != null ) {
List<Id> sobjects = assgnGroupToSobjectMap.get(assgGroupId);
if (sobjects == null) {
sobjects = new List<Id>();
}
sobjects.add(sobjectId);
assgnGroupToSobjectMap.put(assgGroupId, sobjects);
}
}
}
return assgnGroupToSobjectMap;
}
// @return Map{Owner ID -> Assignment Group ID}
@TestVisible private Map<Id, Id> ownersToAssignmentGroups() {
if (ownerToAssgnGroupsMap == null) {
ownerToAssgnGroupsMap = new Map<Id, Id>();
List<Assignment_Group_Queue__c> agqs = new List<Assignment_Group_Queue__c>(
[SELECT Id,Name,Assignment_Group__c,QueueId__c
FROM Assignment_Group_Queue__c
WHERE Active__c = true AND QueueId__c in :sobjectToOwnerMap.values()]
);
for (Assignment_Group_Queue__c agq : agqs) {
ownerToAssgnGroupsMap.put(agq.QueueId__c, agq.Assignment_Group__c);
}
}
return ownerToAssgnGroupsMap;
}
}
@isTest
private with sharing class AssignmentGroupAssistantTest {
@isTest static void testLeadTrigger() {
// Setup test objects
Lead l = createLead();
createGroups();
User u = createTestUser();
Group g = [SELECT Id,Name FROM Group WHERE Name = 'Lead Test Queue'];
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = createAssignmentGroupQueue(ag, g);
Assignment_Group_Member__c member = createAssignmentGroupMember(ag, u);
l.OwnerId = g.Id;
update l;
Lead updatedLead = [SELECT OwnerId FROM Lead WHERE Id = :l.Id][0];
System.assertNotEquals(updatedLead.OwnerId, g.Id);
}
@isTest static void testCaseTrigger() {
// Setup test objects
Case c = createCase();
createGroups();
User u = createTestUser();
Group g = [SELECT Id,Name FROM Group WHERE Name = 'Case Test Queue'];
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = createAssignmentGroupQueue(ag, g);
Assignment_Group_Member__c member = createAssignmentGroupMember(ag, u);
c.OwnerId = g.Id;
update c;
Case updatedCase = [SELECT OwnerId FROM Case WHERE Id = :c.Id][0];
System.assertNotEquals(updatedCase.OwnerId, g.Id);
}
@isTest static void testGroupMembersToUpdate() {
// Setup test objects
Lead l = createLead();
createGroups();
User u = createTestUser();
Group g = [SELECT Id,Name FROM Group WHERE Name = 'Lead Test Queue'];
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = createAssignmentGroupQueue(ag, g);
Assignment_Group_Member__c member = createAssignmentGroupMember(ag, u);
Map<Id, Id> leadOwners = new Map<Id, Id>();
leadOwners.put(l.Id, g.Id);
AssignmentGroupAssistant assistant = new AssignmentGroupAssistant(leadOwners);
// Returns an empty list if owner assignments have not been generated yet
System.assertEquals(assistant.groupMembersToUpdate().size(), 0);
assistant.newOwnerAssignments();
// Returns a list of the Assignment Group Members involved in assignment
System.assertEquals(assistant.groupMembersToUpdate().size(), 1);
System.assertEquals(assistant.groupMembersToUpdate()[0].Id, member.Id);
}
@isTest static void testAssgnGroupsToMembers() {
// Setup test objects
Lead l = createLead();
createGroups();
User u1 = createTestUser();
User u2 = createTestUser();
Group g = [SELECT Id,Name FROM Group WHERE Name = 'Lead Test Queue'];
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = createAssignmentGroupQueue(ag, g);
Assignment_Group_Member__c member1 = createAssignmentGroupMember(ag, u1);
Assignment_Group_Member__c member2 = createAssignmentGroupMember(ag, u2);
// Manually modify Last Assignment to test SOQL sort order
member2.Last_Assigned__c = DateTime.now().addDays(-3);
update member2;
Map<Id, Id> leadOwners = new Map<Id, Id>();
leadOwners.put(l.Id, g.Id);
AssignmentGroupAssistant assistant = new AssignmentGroupAssistant(leadOwners);
// Verify there is a map of ID -> AG-Member list
List<Assignment_Group_Member__c> members = assistant.assgnGroupsToMembers().get(ag.Id);
System.assertEquals(members[0].Id, member2.Id); // List is sorted based on last assignment date
System.assertEquals(members[1].Id, member1.Id);
}
@isTest static void testAssignmentGroupsToSobjects() {
// Setup test objects
Lead l = createLead();
createGroups();
Group g = [SELECT Id,Name FROM Group WHERE Name = 'Lead Test Queue'];
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = createAssignmentGroupQueue(ag, g);
// Change owner to a queue
l.OwnerId = g.Id;
// Setup expected test result
Map<Id, List<Id>> testResult = new Map<Id, List<Id>>();
List<Id> leadList = new List<Id>();
leadList.add(l.Id);
testResult.put(ag.Id, leadList);
Map<Id, Id> leadOwners = new Map<Id, Id>();
leadOwners.put(l.Id, l.OwnerId);
AssignmentGroupAssistant assistant = new AssignmentGroupAssistant(leadOwners);
System.assertEquals(assistant.assignmentGroupsToSobjects(), testResult);
}
@isTest static void testOwnersToAssignmentGroups() {
// Setup test objects
Lead l = createLead();
createGroups();
Group g = [SELECT Id,Name FROM Group WHERE Name = 'Lead Test Queue'];
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = createAssignmentGroupQueue(ag, g);
// Change owner to a queue
l.OwnerId = g.Id;
// Setup expected test result
Map<Id, Id> testResult = new Map<Id, Id>();
testResult.put(g.Id, ag.Id);
Map<Id, Id> leadOwners = new Map<Id, Id>();
leadOwners.put(l.Id, l.OwnerId);
AssignmentGroupAssistant assistant = new AssignmentGroupAssistant(leadOwners);
System.assertEquals(assistant.ownersToAssignmentGroups(), testResult);
}
private static Lead createLead() {
Lead l = new Lead(
LastName = 'Tester',
Company = 'Testco',
Email = 'tester@test.co',
Status = 'New'
);
insert l;
return l;
}
private static Case createCase() {
Case c = new Case(
Status = 'New'
);
insert c;
return c;
}
private static Group[] createGroups() {
Group[] groups = new Group[]{};
Group gLead = new Group(
Name = 'Lead Test Queue',
DeveloperName = 'Lead_Test_Queue',
Type = 'Queue'
);
groups.add(gLead);
Group gCase = new Group(
Name = 'Case Test Queue',
DeveloperName = 'Case_Test_Queue',
Type = 'Queue'
);
groups.add(gCase);
insert groups;
// Avoid mixed DML operation error
System.runAs(new User(Id = Userinfo.getUserId())) {
QueueSobject[] qsos = new QueueSobject[]{};
QueueSobject qLead = new QueueSobject(
QueueId = gLead.Id,
SObjectType = 'Lead'
);
qsos.add(qLead);
QueueSobject qCase = new QueueSobject(
QueueId = gCase.Id,
SObjectType = 'Case'
);
qsos.add(qCase);
insert qsos;
}
return groups;
}
private static Assignment_Group__c createAssignmentGroup() {
Assignment_Group__c ag = new Assignment_Group__c(
Name = 'Test Assignment Group'
);
insert ag;
return ag;
}
private static Assignment_Group_Queue__c createAssignmentGroupQueue(Assignment_Group__c ag, Group g) {
Assignment_Group_Queue__c agq = new Assignment_Group_Queue__c(
Name = g.Name,
QueueID__c = g.Id,
Assignment_Group__c = ag.Id
);
insert agq;
return agq;
}
private static Assignment_Group_Member__c createAssignmentGroupMember(Assignment_Group__c ag, User u) {
Assignment_Group_Member__c member = new Assignment_Group_Member__c(
Assignment_Group__c = ag.Id,
User__c = u.Id
);
insert member;
return member;
}
private static User createTestUser() {
String orgId = UserInfo.getOrganizationId();
String dateString = String.valueof(Datetime.now()).replace(' ','').replace(':','').replace('-','');
Integer randomInt = Integer.valueOf(math.rint(math.random() * 1000000));
String uniqueName = orgId + dateString + randomInt;
User u = new User(
Username = uniqueName + '@test' + orgId + '.org',
Email = uniqueName + '@test' + orgId + '.org',
LastName = 'LastName',
FirstName = 'FirstName',
Alias = uniqueName.substring(18, 23),
ProfileId = UserInfo.getProfileId(),
EmailEncodingKey='UTF-8',
LanguageLocaleKey='en_US',
LocaleSidKey='en_US',
TimeZoneSidKey='America/Los_Angeles',
Country = 'US',
Title = 'Territory Manager'
);
insert u;
return u;
}
}
trigger AssignmentGroupQueueValidation on Assignment_Group_Queue__c (before insert, before update) {
Map<Integer, Assignment_Group_Queue__c> agqsToValidate = new Map<Integer, Assignment_Group_Queue__c>();
Integer index = 0;
for (Assignment_Group_Queue__c agq : Trigger.new) {
// Continue for new AGQs or when AGQ name changes
if (Trigger.isInsert || (agq.Name != Trigger.oldMap.get(agq.Id).Name)) {
agqsToValidate.put(index, agq);
}
index++;
}
// Save on unnecessary SOQL queries
if (agqsToValidate.values().size() == 0) { return; }
Map<String,Group> standardQueues = new Map<String,Group>();
for (Group q : [SELECT Id,Name FROM Group WHERE Type = 'Queue']) {
// Todo: may be problematic when two queues have the same name, but different DeveloperNames
standardQueues.put(q.Name, q);
}
Map<String,String> existingAGQueues = new Map<String,String>();
for (Assignment_Group_Queue__c agq : [SELECT Name, Assignment_Group__r.Name
FROM Assignment_Group_Queue__c]) {
existingAGQueues.put(agq.Name, agq.Assignment_Group__r.Name);
}
for (Integer i : agqsToValidate.keySet()) {
String agqName = agqsToValidate.get(i).Name;
if (standardQueues.containsKey(agqName)) { // AG-Queue Name matches a Salesforce Queue
if (!existingAGQueues.containsKey(agqName)) { // AG-Queue Name is unique among other AG-Queues
Id queueId = standardQueues.get(agqName).Id;
Trigger.new[i].QueueId__c = queueId;
} else {
Trigger.new[i].addError('Assignment Group Queue "' + agqName + '" already connected to another Assignment Group ' + existingAGQueues.get(agqName));
}
} else {
Trigger.new[i].addError('Salesforce Queue cannot be found with the name: ' + agqName);
}
}
}
@isTest
private with sharing class AssignmentGroupQueueValidationTest {
@isTest static void testQueueIDCopied() {
Group q = createQueue();
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = new Assignment_Group_Queue__c(
Name = q.Name,
Assignment_Group__c = ag.Id
);
insert agq;
// Matching Salesforce Queue ID is saved to the AG-Queue
Assignment_Group_Queue__c updatedAGQ = [SELECT QueueId__c,Active__c FROM Assignment_Group_Queue__c WHERE Id = :agq.Id][0];
System.assertEquals(updatedAGQ.QueueID__c, q.Id);
updatedAGQ.Active__c = false;
update updatedAGQ; // Additional trigger coverage
}
@isTest static void testCatchSameName() {
Group q = createQueue();
Assignment_Group__c ag = createAssignmentGroup();
Assignment_Group_Queue__c agq = new Assignment_Group_Queue__c(
Name = q.Name,
Assignment_Group__c = ag.Id
);
insert agq;
// Expect errors when saving another AG-Queue using the same name
Assignment_Group_Queue__c agq2 = new Assignment_Group_Queue__c(
Name = q.Name, // Same Queue Name
Assignment_Group__c = ag.Id
);
Database.SaveResult result = Database.insert(agq2, false);
System.assert(!result.isSuccess());
}
@isTest static void testNonexistentQueue() {
Assignment_Group__c ag = createAssignmentGroup();
// Expect errors when saving an AG-Queue without a corresponding Queue
Assignment_Group_Queue__c agq = new Assignment_Group_Queue__c(
Name = 'Some Queue I Want',
Assignment_Group__c = ag.Id
);
Database.SaveResult result = Database.insert(agq, false);
System.assert(!result.isSuccess());
}
private static Group createQueue() {
Group gLead = new Group(
Name = 'Lead Test Queue',
DeveloperName = 'Lead_Test_Queue',
Type = 'Queue'
);
insert gLead;
System.runAs(new User(Id = Userinfo.getUserId())) {
QueueSobject qLead = new QueueSobject(
QueueId = gLead.Id,
SObjectType = 'Lead'
);
insert qLead;
}
return gLead;
}
private static Assignment_Group__c createAssignmentGroup() {
Assignment_Group__c ag = new Assignment_Group__c(
Name = 'Test Assignment Group'
);
insert ag;
return ag;
}
}
@radhikashet
Copy link

radhikashet commented Oct 3, 2017

Hi all,
Please visit the below link for your RoundRobin record assignments. Its a free app.
R-Robin helps you in automatically assigning your records to the users defined in the queue also considering your leaves/holidays/events. It can be used for any standard and custom object.

Click here

@FlorianGanster
Copy link

Hey,
What's the formula for the third object please?

@FlorianGanster
Copy link

Not working on my side :(

@eskfung
Copy link
Author

eskfung commented Apr 9, 2021

What's the formula for the third object please?

User__r.IsActive is what I originally wrote, but it could be User__r.IsActive && Active__c to simplify this line in the loop.

@FlorianGanster
Copy link

That’s what I thought and wrote, but the apex class : AssignmentGroupAssistantTest
doesn’t work when I run it. I also tried to make a test by importing a list of leads but still doesn’t work

@FlorianGanster
Copy link

These are the 6 errors that I received when I tried to run the apex class mentioned above:

  • Error Message System.NullPointerException: Attempt to de-reference a null object Stack Trace Class.AssignmentGroupAssistantTest.testAssgnGroupsToMembers: line 84, column 1

  • Error Message System.AssertException: Assertion Failed: Expected: {}, Actual: {a191q000000OmmxAAC=(00Q1q000005gL7pEAE)} Stack Trace Class.AssignmentGroupAssistantTest.testAssignmentGroupsToSobjects: line 108, column 1

  • Error Message System.AssertException: Assertion Failed: Same value: 00G1q000003GeJIEA0 Stack Trace Class.AssignmentGroupAssistantTest.testCaseTrigger: line 34, column 1

  • Error Message System.AssertException: Assertion Failed: Expected: 0, Actual: 1 Stack Trace Class.AssignmentGroupAssistantTest.testGroupMembersToUpdate: line 57, column 1

  • Error Message | System.AssertException: Assertion Failed: Same value: 00G1q000003GeJLEA0 Stack Trace | Class.AssignmentGroupAssistantTest.testLeadTrigger: line 17, column 1

  • Error Message | System.AssertException: Assertion Failed: Expected: {}, Actual: {00G1q000003GeJNEA0=a191q000000Omn1AAC} Stack Trace | Class.AssignmentGroupAssistantTest.testOwnersToAssignmentGroups: line 129, column 1

Can you please help with that. This method seems to exactly be what I'm looking for 🙏

@eskfung
Copy link
Author

eskfung commented Apr 12, 2021

Boy, I haven't looked at this code in 6 years, but the tests still pass in my relatively-untouched sandbox. I wouldn't know where to start debugging. It's worth verifying that the test factories (createLead, createCase, createGroups) are successfully creating the corresponding objects, in case your environment has validations that require extra fields.

Good luck. 😬

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment