function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
j-dogj-dog 

Auto-completion date of milestone gets tripped when multiple fields are updated at once...

Hello,

SFDC doesn't have allow updates to milestone completion date so they offered up some apex as a 'delivered' solution to an Idea (https://success.salesforce.com/ideaview?id=08730000000HS83AAG).  So, trying to follow their example, I modified it to match based on the conditions we use. We are trying to move to milestones.  However, currently, we use three date/time fields (Response, Restore, Resolve) and then we have three resulting fields that cacluate (open date/time - response time). Pretty basic.

So, the modified code, below, works well when I enter a response time, click save, enter a restore time, click save, and enter a resolve time and click save.  The completion data will be filled out for the correct milestone.  This is great.  However, there are times when a case can be responded to, restored, and resolved at the same time so the agent will populate each of those date/time fields and click save.  What happens in that situation is that the date/time for each of those fields is set, however, the resulting fields are blank (all it does is subtracked response time from create time, and restore time from create time, and resolve time from create time (6 fields in total (3 date/time, 3 resulting times).  When this happens, the milestones do not have the completion date filled out.  We can edit be clearing the date/time fields and entering them one at a time, saving each time.

I'm not sure why this is happening.  Any thoughts, advice?

trigger completeResponseMilestone on Case (after update) {
    // Cannot be a portal user
    String responseMilestoneName;
    if (UserInfo.getUserType() == 'Standard'){
        DateTime completionDate = System.now();
            List<Id> updateCases = new List<Id>();
            for (Case c : Trigger.new){
                // Response
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 4 - Low')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Default Low Response';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 3 - Minor')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Default Minor Response';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 2 - Major')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Default Major Response';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 1 - Critical')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Default Critical Response';
                    updateCases.add(c.Id);
        }
    if (updateCases.isEmpty() == false)
        milestoneUtils.completeMilestone(updateCases, responseMilestoneName, completionDate);
    }
}

It seems like the trigger is blocking is blocking something, but I don't know what, or how.  I've trimmed down the code just to focus on response time.  I would like to use the same code to also handle restore and resolve, just continue with the if statements and set restore/resolve variables.  It seemed to work, but when I ran into this issue, I thought maybe trying to manage all three fields was the issue.  But it doesn't seem to be.

Thanks for the help...
James LoghryJames Loghry
Can you post your updates to the milestoneUtils class, using the code format button  (< >)?  I suspect that's where the issue is.
James LoghryJames Loghry
After looking at this again, your trigger is very serial.  Meaning, that the trigger handles 1 case at a time, and the way it's written it will only work with 1 response at a time  (E.g. you call the complete milestones method outside of the for loop).  If you want the trigger to handle multiple milestones OR even multiple cases at the same time, you'll need to adjust the IF statements in your for loop, and utilize maps to make your trigger function in a bulk capacity.  I'll try to write up some changes for you later today, but until then I would suggest taking a closer look at your trigger and how you could utilize those maps to better "bulkify" your trigger. For tips on how to do this, read up on the Apex 15 commandments: https://developer.salesforce.com/blogs/developer-relations/2015/01/apex-best-practices-15-apex-commandments.html
j-dogj-dog
James,

I appreciate the help.  Here's the milestoneUtils class you asked for (taken from https://developer.salesforce.com/page/Case_Milestones_Utilities_Class)
 
public class milestoneUtils {
    
    public static void completeMilestone(List<Id> caseIds, String milestoneName, DateTime complDate) {
      
    List<CaseMilestone> cmsToUpdate = [select Id, completionDate
                       from CaseMilestone cm
                       where caseId in :caseIds and cm.MilestoneType.Name=:milestoneName and completionDate = null limit 1];
    if (cmsToUpdate.isEmpty() == false){
      for (CaseMilestone cm : cmsToUpdate){
        cm.completionDate = complDate;
      }
      update cmsToUpdate;
    } // end if
  }

I did try calling the milestoneUtils more than one time in the trigger (one each for Response, Restore, and Resolution time, each with their own variable (responseMilestoneName, restoreMilestoneName, etc) but that is were I first tried updating all three at one time and so I removed all code for restore and resolution and just stuck with response.  And still have the same issue.  Thanks for the reference links, I will read through them and see if I can understand.

Again, thanks for the help.
James LoghryJames Loghry
Here's what I came up with.  I haven't tested it or even tried to compile it, so there's likely some error's you'll have to work through.  However, it takes the case milestones method you're using and should bulkify it, allowing you to update multiple cases at the same time.  I'm not sure how well it handles multiple "responses" / severities though, because you're only keying off a single field. By that I mean, I'm not sure how your agents currently handle multiple responses on the same case without multi-picklists, checkboxes, or junction objects.  Anyways, you can take the code below and modify it to suit your needs, it should get you on the right track.
 
trigger completeResponseMilestone on Case (after update) {
    // Cannot be a portal user
    String responseMilestoneName;
    Map<Id,List<String>> caseIdToMilestonesMap = new Map<Id,List<String>>();


    Map<String,String> severityToMilestonesMap = new Map<String,String>{'Severity 4 - Low','Severity 3 - Minor','Severity 2 - Major','Severity 1 - Critical'};

    if (UserInfo.getUserType() == 'Standard'){
        DateTime completionDate = System.now();
        List<Id> updateCases = new List<Id>();
        for (Case c : Trigger.new){
            // Response
            if(c.Status != 'Closed' && c.Status != 'Closed as Duplicate' && c.SlaStartDate <= completionDate && c.SlaExitDate == null && c.Response_Time__c != null){
                String milestoneName = severityToMilestonesMap.get(c.Severity__c);
                if(!String.isEmpty(milestoneName)){
                    List<String> milestones = caseIdToMilestonesMap.get(c.Id);
                    if(milestones == null){
                        milestones = new List<String>();
                        caseIdToMilestonesMap.put(c.Id,milestones);
                    }
                    milestones.add(milestoneName); 
                }
            }
        }

        List<CaseMilestone> milestonesToUpdate = new List<CaseMilestone>();
        for(CaseMilestone cm : 
            [Select 
                Id
                ,CompletionDate
                ,MilestoneType.Name 
                ,CaseId
             From 
                CaseMilestone 
             Where 
                CaseId in :caseIdToMilestonesMap.keySet()
                And completingDate = null]){
            for(String currentMilestone : caseIdToMilestonesMap.get(cm.CaseId)){
                if(cm.MilestoneType.Name == currentMilestone){
                    cm.CompletionDate = completionDate;
                    milestonesToUpdate.add(cm);
                }
            }
        }
        update milestonesToUpdate;
    }
}

 
j-dogj-dog
James,
  1. Could you explain, high level, what is happening here?  I've only just leared what 'instantiating an object from a class' means and I haven't made it to maps, yet.
  2. You mention this code should help with updating multiple cases at the same time.  I want to confirm we are on the same page.  My issue was that within the same case, if there are multiple field updates made at the same time (clicking save after populating these three date/time fields) weirdness would happen.  But making one field update at a time and clicking save between them seemed to work just fine.  And for 95% of the cases that come in, this is what happens, in order, while working a case. Working a case is pretty serial in this regard.
    1. Response Time
    2. Restore Time
    3. Resolve Time
  3. "I'm not sure how well it handles multiple "responses" / severities though, because you're only keying off a single field. "
    1. A case only has one severity.  Based on that severity, a set of milestones will apply to the case. For example, if the case is "Severity 1 - Critical"
      1. Default Response Milestone = 15 mins
      2. Default Restore Milestone = 2 hours
      3. Default Resolve Milestone = 45 days
    2. As the agent works the case, they will enter a date/time in the Response Time field.  It's from this action that I would like the trigger to do its thing. Serially, it seems to work well.
    3. As the agent restores the case, they will enter a date/time in the Restore Time field. It's from this action that I would like the trigger to do its thing. Serially, it seems to work well.
    4. As the agent resolves the case, they will enter a date/time in the Resolution Time field. It's from this action that I would like the trigger to do its thing. Serially, it seems to work well.
  4. What doesn't work well is when an agent gets a call, it's a new case, they help the customer and resolve the issue, they enter a case after the fact, and enter all three timers at the same time and click save.  Or, working a case, restoring the issue was also the final resolution (customer doesn't need to wait for a bug fix) they agen can enter the Restore Time and the Resolution Time at the same time and click save.
  5. It doesn't make sense to me why calling milestoneUtils.completeMilestone three times (response, restore, resolve) in the same trigger wouldn't work.
  6. Assuming we are on the same page, and I just don't know it (hehe), would I just need to repeat code block lines 14-23? One block for restore and one block for resolve?
  7. In looking at the severityToMilestoneMap, it seems like it would need some more mapping, no? Since each severity brings in a set of 3 milestones like below.
    1. Severity 1 - Critical
      1. Default Critical Response Milestone
      2. Default Critical Restore Milestone
      3. Default Critical Resolve Milestone
    2. Severity 2 - Major
      1. Default Major Response Milestone
      2. Default Major Restove Milestone
      3. Default Major Resolve Milestone
    3. etc...
I don't know if the above helps clarifes, muddies, or confirms, but I appreciate your help. :)
j-dogj-dog

I wrote it out the way I would have expected it to work.  Maybe this will help make the situation clearer.

milestoneUtils Class
 

public class milestoneUtils {
    public static void completeMilestone(List<Id> caseIds, String milestoneName, DateTime complDate) {
        List<CaseMilestone> cmsToUpdate = [select Id, completionDate
                           from CaseMilestone cm
                           where caseId in :caseIds
                           and cm.MilestoneType.Name=:milestoneName
                           and completionDate = null limit 1];
        if (cmsToUpdate.isEmpty() == false){
            for (CaseMilestone cm : cmsToUpdate){
                cm.completionDate = complDate;
            }
            update cmsToUpdate;
        } // end if
    }
}

completeMilestone Trigger:
trigger completeResponseMilestone on Case (after update) {

    // Cannot be a portal user
    String responseMilestoneName;
    String restoreMilestoneName;
    String resolveMilestoneName;
    if (UserInfo.getUserType() == 'Standard'){
        DateTime completionDate = System.now();
            List<Id> updateCases = new List<Id>();
            for (Case c : Trigger.new){
                // Response
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 4 - Low')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Low Response';
                    updateCases.add(c.Id);                  
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 3 - Minor')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Minor Response';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 2 - Major')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Major Response';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 1 - Critical')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Response_Time__c != null)))
                    responseMilestoneName = 'Critical Response';
                    updateCases.add(c.Id);
                //Restore
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 4 - Low')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Restore_Time__c != null)))
                    restoreMilestoneName = 'Low Restore';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 3 - Minor')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Restore_Time__c != null)))
                    restoreMilestoneName = 'Minor Restore';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 2 - Major')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Restore_Time__c != null)))
                    restoreMilestoneName = 'Major Restore';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 1 - Critical')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Restore_Time__c != null)))
                    restoreMilestoneName = 'Critical Restore';
                    updateCases.add(c.Id);
                //Resolve
                 if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 4 - Low')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Resolution_Time__c != null)))
                    resolveMilestoneName = 'Low Resolve';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 3 - Minor')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Resolution_Time__c != null)))
                    resolveMilestoneName = 'Minor Resolve';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 2 - Major')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Resolution_Time__c != null)))
                    resolveMilestoneName = 'Major Resolve';
                    updateCases.add(c.Id);
                if (((c.Status != 'Closed')&&(c.Status != 'Closed as Duplicate'))&& (c.Severity__c == 'Severity 1 - Critical')&&((c.SlaStartDate <= completionDate)&&(c.SlaExitDate == null) && (c.Resolution_Time__c != null)))
                    resolveMilestoneName = 'Critical Resolve';
                    updateCases.add(c.Id);
        }
    if (updateCases.isEmpty() == false)
        milestoneUtils.completeMilestone(updateCases, responseMilestoneName, completionDate);
    if (updateCases.isEmpty() == false)
        milestoneUtils.completeMilestone(updateCases, restoreMilestoneName, completionDate);
    if (updateCases.isEmpty() == false)
        milestoneUtils.completeMilestone(updateCases, resolveMilestoneName, completionDate);
    }
}

This works fine when we update the timer fields one at a time:
User-added image
But it doesn't work if we try to update two or more timer fields at a time.  When we do, it looks like this (I took the above example and tried to update "Time to Restore" and "Time to Resolution" at the same time:
User-added image
The Restore Time field is just a formula that is simple "Time to Restore" - "Open Date".  But it's not able to populate when the trigger interferes in this scenario. And, the milestones are not completed. If I clear those two entries, then enter them one at a time, it works.

I tried sending a list of milestone names to the milestoneUtils class and then loop through them, but that didn't work.
j-dogj-dog
James,

I took your code and modified it a bit. The severtyToMilestones map wasn't working and I'm not yet sure how to correct so I took the brute force approach and just working with Low Severity milestones (Low Response, Low Restore, Low Resolve).

The part you wrote about updating the caseIdToMilestonesMap seems to work well, I am able to update more than one milestone if more than one date/time fields is updating.

However, it does not work the first time I update one or more date/time fields.  For example, if I update Response Time, Restore Time, and Resolve Time all at the same time, it doesn't work, it looks like the screenshots in my previous post. BUT, if I clear all the entries and save, and then enter them all again, it works.  Any thoughts why that might be?  It's like the trigger doesn't work on the first save after the case is created.  But after that, it seems OK.

Thanks for your help.  Current code below...
 
trigger completeResponseMilestone on Case (after update) {
    // Cannot be a portal user
    String responseMilestoneName;
    Map<Id,List<String>> caseIdToMilestonesMap = new Map<Id,List<String>>();

    if (UserInfo.getUserType() == 'Standard'){
        DateTime completionDate = System.now();
        List<Id> updateCases = new List<Id>();
        List<String> milestoneName = new List<String>();
        for (Case c : Trigger.new){
            // Response
            if(c.Status != 'Closed' && c.Status != 'Closed as Duplicate' && c.SlaStartDate <= completionDate && c.SlaExitDate == null && c.Response_Time__c != null && c.Severity__c == 'Severity 4 - Low'){
                milestoneName.add('Low Response');          
                caseIdToMilestonesMap.put(c.Id,milestoneName);                  
            }
            // Restore
            if(c.Status != 'Closed' && c.Status != 'Closed as Duplicate' && c.SlaStartDate <= completionDate && c.SlaExitDate == null && c.Restore_Time__c != null && c.Severity__c == 'Severity 4 - Low'){
                milestoneName.add('Low Restore');
                caseIdToMilestonesMap.put(c.Id,milestoneName);
            }
            // Resolve
            if(c.Status != 'Closed' && c.Status != 'Closed as Duplicate' && c.SlaStartDate <= completionDate && c.SlaExitDate == null && c.Resolution_Time__c != null && c.Severity__c == 'Severity 4 - Low'){
                milestoneName.add('Low Resolve');
                caseIdToMilestonesMap.put(c.Id,milestoneName);
            }
        }

        List<CaseMilestone> milestonesToUpdate = new List<CaseMilestone>();
        for(CaseMilestone cm : 
            [Select 
                Id
                ,CompletionDate
                ,MilestoneType.Name 
                ,CaseId
             From 
                CaseMilestone 
             Where 
                CaseId in :caseIdToMilestonesMap.keySet()
                And completionDate = null]){
            for(String currentMilestone : caseIdToMilestonesMap.get(cm.CaseId)){
                if(cm.MilestoneType.Name == currentMilestone){
                    cm.CompletionDate = completionDate;
                    milestonesToUpdate.add(cm);
                }
            }
        }
        update milestonesToUpdate;
    }
}

 
j-dogj-dog
Seems to work solidly if I wait 1 minute or more and make at least one update to the case in some other field first, save, then update the date/time fields.