Showing posts with label Cases. Show all posts
Showing posts with label Cases. Show all posts

Thursday, February 2, 2012

Detecting the sObject type of Id's from ambiguous fields

When dealing with the OwnerId field, it is important to check if it is a User or a Group sObject you are dealing with before you start using it or you'll get errors. The Task object also has a few ambiguous fields that match up multiple Objects like the WhoId and the WhatId. In triggers and in other logic you will sometimes need to detect what the object is to either exclude or include it in the next bit of code. We dont want to iterate through code and update records if we don't need to, it will bog down the system. 


I have seen some of the ways out there that are used, particularly the one where you check if the first 3 letters of the ID is '500' or '300' but i do not think this is an accurate way of detection. I want one that the system tells me for sure what object it is. Unfortunately its not as dynamic as i would like it to be due to limitations in APEX, but it is still a much better detection than the above. 


I first made a helper class called GlobalHelper_IsSobjectType and populated it with a few methods that look like the following. It takes in the Id and after a check to make sure that an Id was actually passed and is not null, it try's to create an Object of that Type. If it can it will not fall into the Catch and will return true, other wise it will error and return false, but it will not error in a way that will halt your program from finishing(thus why its in a try catch). 




I made one for Account, Contact, Lead, User, and Opportunity since those are the ones I typically have to detect. When it comes to the OwnerId field I can just use the one for User since if it returns false i know its a group. I also found that I needed to Overload the method so it would work with string as well as Id: 




Ideally I was hoping to make the above but make it more dynamic so you pass the object type you wanted to detect as well as the id for the detecting. That way you would only need one method instead of one for each Object, but I have yet to find a way to make it work. Now of course you need to test the code, and I prefer as close to 100% coverage as possible so:






Questions?  
Twitter: @SalesForceGirl  or Facebook


Wednesday, March 16, 2011

How to 'Queue' sObjects based on SLA and Priority

The other day I was given the task of creating a button on the Case sObject that gives the User the next Case from the Queue's that they belong to. It would be based on SLA time and priority, which would be generated from a custom setting and formula field on Case sObject. The Custom setting would hold priority ratings and SLA times for each Queue, and the formula field would show the age of the Case in hours from last-mod date. The end result is that it will take  the oldest Case with the highest Priority if SLA time is meet, otherwise it will take the first Case out of SLA based on Priority. The custom setting can be a list or a hierarchy, it just depends on if you want to be able to control it down to the user level, where as the list is more of an org-wide default. This can also be set up for any sObject that can have a Queue instead of just the Case sObject like in my example.

So on the Case page it would look like this:
and when they clicked the 'Take Next Ticket' link it would return the next ticket by SLA and priority, and automatically assigns the user as the owner of the Case. Then once they close the case and click the 'Take Next Ticket' link it will all repeat over and over. Ensuring that no one has Cases waiting to in their personal Queues and that the oldest/highest priority gets handled first. 

Custom Setting Setup:

Example Items:
NamePriority(1-10)SLA(min)
HighPriorityQueue1012
AnotherQueueName524
LowPriorityQueue136
    

Formula Field Setup:

The field needs to be a number and the formula to calculate the age of the Case in mins:

Now the VF page:

The page itself will be blank, unless you want to show errors if anything fails, like no more tickets in the Queues that the rep belongs to. 

<apex:page standardController="Case" extensions="TakeNextTicketCon" standardStylesheets="false" tabstyle="Case" action="{!TakeNextTicket}">
<style>
      #messageWrap{width:50%margin:autofont-size:15px;}
      #linkWrap{text-align:center;}
      ul li{color:redtext-decoration:nonelist-style:nonepadding:0pxmargin:0text-align:center;}
</style>
      <apex:form >
            <div id="messageWrap">
                  <apex:messages id="messagesblock" />
            </div>
            <div id="linkWrap">
                  <apex:outputlink value="/{!Case.id}">Go Back</apex:outputlink>
                  &nbsp;|&nbsp;
                  <apex:commandLink action="{!TakeNextTicket}">Take-Next-Ticket</apex:commandLink>
            </div>
      </apex:form>
</apex:page>

Now the fun stuff, the Classes. In mine i split the classes up into the main controller (TakeNextTicketCon.cls) and a helper class (TakeNextTicketHelper.cls) and then finally the test Class (TakeNextTicketHelper). I tired to set up the classes so that all the info and action methods are in the Main class, and the everything else is in the helper. It was difficult to get the info required for this due to the way that salesforce has the Queue's set up in the schema. 

To get the Queues that a user belongs to for a particular sObject, you need to first query GroupMemeber where UserOrGroupId = User Id
GroupMember[] gm = [Select Id, GroupId From GroupMember where UserOrGroupId =:sInUserId limit 50];

Then you need to take that list and convert it to a string list of GroupId's and use it to query the Group sObject where type = Queue and the id is in the list of id's that we just create from the GroupMemeber sObject. 
Group[] g = [select id, Name from Group where Type = 'Queue' and id IN :lstInGroupId limit 50];

Then once you get that list and again convert it to a string list of Id's and use it in the next query of the QueueSobject where sobjectType = Case and QueueId is in the list of Group id's.
QueueSobject[] q = [Select Id, QueueId, Queue.Name From QueueSobject where SobjectType = 'Case' and QueueId IN:lstInGroupIdlimit 50];

fun right? If you look at the sObject in schema, you'll see why i did it that way, but you'll see another way to traverse the sObjects in an upcoming post on making the User/Queue edit more manageable. But lets take a look at the classes i used to power this little link of wonders, just note that I will not be explaining the classes, just showing them. If you need help understanding what or how I did something feel free to contact me and I will brake it down further.

TakeNextTicketCon:

public with sharing class TakeNextTicketCon {
      
      private final Case cse;
      
      public TakeNextTicketCon(ApexPages.StandardController stdController)
      {
        this.cse = (Case)stdController.getRecord();
    }
   
      public Case NextTicket{get;set;} 
      
      public List<QueuePriority__c> objQueueOrder
      {
            get{returnTakeNextTicketHelper.Priority(TakeNextTicketHelper.FindQueues(TakeNextTicketHelper.CurrentUserGroups(TakeNextTicketHelper.FindGroupIdsFromMember(UserInfo.getUserId()))));}
            set;
      }
      /// <summary>
    /// GETS THE NEXT TICKET BASED ON QUEUE PRIORITY AND LAST MOD OF TICKETS IN QUEUE
    /// </summary>
    /// <returns>NEXT TICKET /CURRENT TICKET IF NONE TO GET</returns>
      public pageReference TakeNextTicket()
      {
            NextTicket = TakeNextTicketHelper.NextCase(TakeNextTicketHelper.NextCaseList(objQueueOrder), objQueueOrder);
            
            TakeNextTicketHelper.ChangeOwnerOnCase(UserInfo.getUserId(),NextTicket);
            if(NextTicket != null)
            {
                  PageReference ref = new PageReference('/' + NextTicket.id);
                  ref.setRedirect(true);
                  return ref; 
            }
            ApexPages.Message msg = new ApexPages.Message( ApexPages.Severity.ERROR,'Opps.... All queues are empty... but not for long I am sure :-)');
            ApexPages.addMessage(msg);
            return null;
      }
}

TakeNextTicketHelper:

public with sharing class TakeNextTicketHelper {
      
      /// <summary>
    /// CREATES AN ID LIST FROM PASSED SOBJECT
    /// </summary>
    /// <param name="lstInSobject">LIST TO DEDUP AND ADD</param>
    /// <returns>STRING ID LIST</returns>
      public static List<String> AddToIdListFromGM(List<GroupMember> lstInSobject)
      {
            List<String> idList = new List<string>();
            if(GlobalHelper.CheckForNotNull(lstInSobject))
            {
                  for(GroupMember obj: lstInSobject)
                  {
                        if(obj != null)
                        {
                              if(!GlobalHelper.ContainsItem(obj.GroupId, idList))
                              {
                                    idList.add(obj.GroupId);
                              }
                        }
                  }
                  return idList;
            }
            return null;
      }
      /// <summary>
    /// CREATES AN ID LIST FROM PASSED SOBJECT
    /// </summary>
    /// <param name="lstInSobject">LIST TO DEDUP AND ADD</param>
    /// <returns>STRING ID LIST</returns>
      public static List<String> AddToIdListFromQS(List<QueueSobject> lstInSobject)
      {
            List<String> nameList = new List<string>();
            if(GlobalHelper.CheckForNotNull(lstInSobject))
            {
                  for(QueueSobject obj: lstInSobject)
                  {
                        if(obj != null)
                        {
                              if(!GlobalHelper.ContainsItem(obj.Queue.Name, nameList))
                              {
                                    nameList.add(obj.Queue.Name);
                              }
                        }
                  }
                  return nameList;
            }
            return null;
      }
      /// <summary>
    /// CREATES AN NAME LIST FROM PASSED SOBJECT
    /// </summary>
    /// <param name="lstInSobject">LIST TO DEDUP AND ADD</param>
    /// <returns>STRING NAME LIST</returns>
      public static List<String> AddToNameList(List<QueuePriority__c> lstInSobject)
      {
            List<String> nameList = new List<string>();
            if(GlobalHelper.CheckForNotNull(lstInSobject))
            {
                  for(QueuePriority__c obj: lstInSobject)
                  {
                        if(obj != null)
                        {
                              if(!GlobalHelper.ContainsItem(obj.Name, nameList))
                              {
                                    nameList.add(obj.Name);
                              }
                        }
                  }
                  return nameList;
            }
            return null;
      }
      /// <summary>
    /// FINDS GROUP IDS BASED ON ID LIST PASSED
    /// </summary>
    /// <param name="lstInGroupId">LIST TO SELECT GROUP IDS FROM</param>
    /// <returns>STRING ID LIST</returns>
      public static List<string> CurrentUserGroups(List<String> lstInGroupId)
      {
            if(GlobalHelper.CheckForNotNull(lstInGroupId))
            {
                  try
                  {
                        Group[] g = [select id, Name from Group where Type = 'Queue' and id IN :lstInGroupId limit 50];
                              return GlobalHelper.AddItemsToIdList(g);
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - CurrentUserGroups - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS QUEUES BASED ON ID LIST PASSED
    /// </summary>
    /// <param name="lstInGroupId">LIST TO SELECT GROUP IDS FROM</param>
    /// <returns>SOBJECT LIST OF QUEUES</returns>
      public static List<QueueSobject> FindQueues(List<String> lstInGroupId)
      {
            if(GlobalHelper.CheckForNotNull(lstInGroupId))
            {
                  try
                  {
                        QueueSobject[] q = [Select Id, QueueId, Queue.Name From QueueSobject where SobjectType = 'Case' and QueueIdIN:lstInGroupId limit 50];
                        return q;
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - FindQueues - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS GROUP IDS BASED ON ID FROM USER
    /// </summary>
    /// <param name="sInUserId">USER ID</param>
    /// <returns>STRING ID LIST</returns>
      public static List<String> FindGroupIdsFromMember(string sInUserId)
      {
            if(GlobalHelper.CheckForNotNull(sInUserId))
            {
                  try
                  {
                        GroupMember[] gm = [Select Id, GroupId From GroupMember where UserOrGroupId =:sInUserId limit 50];
                        if(GlobalHelper.CheckForNotNull(gm))
                        {
                              return TakeNextTicketHelper.AddToIdListFromGM(gm);
                        }
                        ApexPages.Message msg = new ApexPages.Message( ApexPages.Severity.ERROR,'Oops... no queues are assigned to you... please contact your manager and they will assign you to one.');
                        ApexPages.addMessage(msg);
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - FindGroupIdsFromMember - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS PRIORITY BASED ON NAME LIST PASSED
    /// </summary>
    /// <param name="lstInQueueNames">QUEUE NAME SOBJECT LIST</param>
    /// <returns>SOBJECT LIST OF QUEUE PRIORITY</returns>
      public static List<QueuePriority__c> Priority(List<QueueSobject> lstInQueueNames){
            if(GlobalHelper.CheckForNotNull(lstInQueueNames))
            {
                  try
                  {
                        QueuePriority__c[] qp = [select id, name, Priority__c, SLA_mins__c from QueuePriority__c where NameIN:TakeNextTicketHelper.AddToIdListFromQS(lstInQueueNames) order by Priority__c DESC limit 50];
                        if(GlobalHelper.CheckForNotNull(qp))
                        {
                              return qp;
                        }
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - Priority - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS NEXT CASES 
    /// </summary>
    /// <param name="lstInPriority">LIST OF QUEUE PRIORITY</param>
    /// <returns>SOBJECT LIST OF CASES</returns>
      public static List<Case> NextCaseList(List<QueuePriority__c> lstInPriority)
      {
            if(GlobalHelper.CheckForNotNull(lstInPriority))
            {     
                  try


                  {
                        Case[] c = [select id, caseNumber, Owner.Name, Time_in_Queue__c from Case where Owner.NameIn:TakeNextTicketHelper.AddToNameList(lstInPriority) and status != 'Closed' order by Time_in_Queue__c DESC limit 200];
                        return c;
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - NextCaseList - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS NEXT CASE 
    /// </summary>
    /// <param name="lstInCase">LIST OF CASES</param>
    /// <param name="lstInPriority">LIST OF QUEUE PRIORITY</param>
    /// <returns>CASES SOBJECT</returns>
      public static Case NextCase(List<Case> lstInCase, List<QueuePriority__c> lstInPriority)
      {
            if(GlobalHelper.CheckForNotNull(lstInPriority))
            {
                  for(QueuePriority__c qp : lstInPriority)
                  {
                        for(Case c : lstInCase)
                        {
                              if(c.Owner.Name == qp.Name)
                              {
                                    if(c.Time_in_Queue__c > qp.SLA_mins__c)
                                    {
                                          return c;
                                    }
                              }
                        }
                  }
                  for(QueuePriority__c qp : lstInPriority)
                  {
                        for(Case c : lstInCase)
                        {
                              if(c.Owner.Name == qp.Name)
                              {
                                    return c;
                              }
                        }
                  }
                  
            }
            return null;
      }
      /// <summary>
    /// CHANGES CASE OWNER TO CURRENT USER 
    /// </summary>
    /// <param name="sInUserId">USER ID</param>
    /// <param name="objInCase">CASE OBJ</param>
      public static void ChangeOwnerOnCase(String sInUserId, Case objInCase)
      {
            if(GlobalHelper.CheckForNotNull(sInUserId) && objInCase != null)
            {
                  try
                  {
                        objInCase.OwnerId = sInUserId;
                        update objInCase;
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - ChangeOwnerOnCase - Error: ' + e);
                  }
            }
      }
}

The Test class i will leave up to you, in my org, we try to get as close to 100% coverage as possible, and i recommend the same for your org as well. If you don't, and just go for 75% coverage, you will find issues down the line with not being fully covered. Remember the rule of 75% coverage is an org standard not per class, so if a few classes bring the average down then you need to go back and fix old code. So its best to take the extra hour and try to get the most coverage as possible in the project you are working on.
Once all the classes are done, you'll still need to add it to the case as a link then add the link to the page layout.

Once you add it as a button/link on the case sObject it will be available to be added via the page-layout edit page.
Questions? 
Twitter: @SalesForceGirl  or Facebook