Pogo69's Blog

March 30, 2012

Case Management – How to Implement Block Increment Rules in Time Based Contracts

Filed under: C#, CRM, Cutting Code — pogo69 [Pat Janes] @ 10:47

For our clients with  Time-based Service Contracts, we have some very specific Business Rule requirements to implement.  This ensures that adequate time is allocated to a Case, no matter how trivial, to cater for the administrative overheads incurred.

The Requirements

  1. Each Case must consume a minimum of 60 mins allotment
  2. Each Case must consume a multiple of 15 mins after the initial 60 mins

For example:

Accrued Billable Time Total Time upon Case Resolution
23 mins 60 mins
47 mins 60 mins
1 hr 13 mins 1 hr 15 mins
2 hrs 47 mins 3 hrs

The first two examples meet Rule #1 – minimum of 60 mins.

The second two examples meet Rule #2 – multiple of 15 mins after the initial minimum 60 mins – that is, rounded up to the nearest 15 min interval.

Overview

Time-Based Contracts -> Accrued Time -> Sum of ActualDurationMinutes in Closed Activities

Time-based Contracts manage allotment in accrued minutes.

When a Case attached to a Time-based Contract is resolved, the CRM aggregates the sum of all ActualDurationMinutes values in Closed Activities attached to that Case.  This includes the in-built Activity Types such as Email, Task etc; and also includes Custom Activity Types.  I make a point of mentioning the inclusion of Custom Activity Types here, as it is not clear in any of the documentation that I have read and there appears to be some subsequent confusion in the CRM Developer community.

In order to Close a Case (incident), one must create an IncidentResolution entity instance and pass it, and the required Status (usually 5 == ‘Problem Solved’) to a CloseIncidentRequest.  The IncidentResolution entity must contain two attributes of interest to the Case Resolution process:

  • IncidentId – the GUID identifier of the Case that we wish to Resolve (Close)
  • TimeSpent – the total number of accrued minutes pertaining to the Case being resolved.  This is the amount of allotment that will be decremented from the Contract Line assigned to the Case

TimeSpent is calculated by (the equivalent of) the following:

int timeSpent =
 (
  from a in xrm.ActivityPointerSet
  where a.RegardingObjectId.Id == caseId
  && a.StateCode == 0
  && a.IsBilled.GetValueOrDefault() == false
  select a.ActualDurationMinutes.GetValueOrDefault()
 ).Sum();

Thus, the Total Time Spent in a Case is calculated to be the sum of the Duration of each un-billed Closed Activity attached to that Case.  So; in order to influence this process to support our Business Rules, we need to add time to that calculation.

There are two ways to achieve this:

  1. Attach additional activities to the Case prior to the Case Resolution process (when the Case Resolution dialog box pops up, the calculation has already been done – thus to influence the Time Spent at this stage, you must create the additional activities before the User attempts to Resolve the Case
  2. Change the value of TimeSpent in the CaseResolution entity instance prior to the Case Resolution process taking place

In order to keep things neat and tidy; and also to ensure that I reconcile the calculations at the appropriate time, I have chosen to implement a combination of both.

Implementation

Time Sheets – A Quick Diversion

While not strictly relevant to this article, I will quickly mention our Time Sheeting process in CRM.  Prior to the introduction of Time-based contracts (we previously only dealt with ‘Number of Cases’ allotment types) we introduced Time Sheeting, implemented as a Custom entity attached to the Case.  The sum of durations in all attached Time Sheets contributed to the number of Cases decremented from the client’s allotment (2 hrs per Case).

With Time-based Contracts, we needed to move to an activity-based mechanism.

With 20-20 hindsight, I would have created the Time Sheet entity as a Custom activity, however:

  1. I didn’t create (or specify) it
  2. Hindsight was not available to those involved at the time
  3. What good does that do us now?

So, in order to:

  1. Ensure that the data entry process is the same regardless of Time-based or Number-of-Case-based Cases
  2. Enable the new activity based calculations to work as expected

I created a Custom activity type (Time Sheet Activity) and wrote a plugin to (re-)generate Closed activities for each Time Sheet attached to the Case every time a Time Sheet is Created, Updated or Deleted.  This allows the times entered into the Time Sheets (that we were already entering anyway) to be reflected (or mirrored) in the total time assigned to activities.

NB: I only create the Time Sheet Activity entity instances for Cases to which a Time-based Contract is attached.

Incident Resolution Process – Timing

Before I move onto the good stuff, I will also quickly mention the Case Resolution process, to give a better understanding of what we are doing and the why of the when.

int decrement = 0;
decrement = this.CalculateAllotmentToDecrement(originalIncident, incidentResolution, contract, context);
if (decrement != 0)
{
 this.DecrementAllotment((Guid)originalIncident["contractdetailid"], decrement, context);
}
this.CreateIncidentResolutionActivity(incidentResolution, context);
this.BillIncidentResolutionActivities(businessEntityMoniker.Id, 1, context);
base.SetState(businessEntityMoniker, 1, status, context);

So, the Case Resolution process is as follows:

  1. Calculate Allotment to decrement.  NB: For Time-based Contracts, this is taken to be the value of TimeSpent within the IncidentResolution entity instance passed in from the Case Resolution dialog.  It is NOT re-calculated as part of the Case Resolution process
  2. Decrement the Allotment
  3. Create IncidentResolution entity instance (up to this point it has been constructed, but NOT Created within the CRM)
  4. Bill the Activities attached to the Case
  5. Close the Case
I originally attempted to influence this process by intercepting the pre-Create message for IncidentResolution, however as can be see from the above, this is too late!!  Hence, the final solution is implemented by intercepting the pre-Close message for Incident (Case).

Activity Duration Reconciliation and Re-calculation of TimeSpent

The basic steps of the process (that I chose to implement) are:

  1. Check that the requested Status is ‘Problem Solved’; if not, bail.  This only applies to successfully resolved Cases
  2. Find Contract Template; attached to the Contract; attached to the Incident (identified in IncidentResolution passed to the CloseIncidentRequest)
  3. Check that Contract is Time-based allotment; if not, bail
  4. Sum the ActualDurationMinutes of each un-billed, Closed Activity attached to the Case (yes, this is theoretically the same as the current value of TimeSpent in the IncidentResolution, but I like to be thorough)
  5. Round up TotalTime to 60 mins if it is currently lower
  6. Round up TotalTime to the nearest 15 min increment, as necessary if currently exceeds 60 mins
  7. If there is a deficit between the TotalTime calculated and that passed in, continue
  8. Create a “Dummy” Time Sheet activity for the deficit – this is not strictly necessary, as the system calculations have already been made, however I like it, as it makes it more visually obvious in the CRM front-end what is going on
  9. Modify TimeSpent in the IncidentResolution entity being passed through via the Close message.  NB: it is this step that influences the allotment quantity that will be applied to the relevant Contract Line
public class MyPlugin : IPlugin
{
 public void Execute(IServiceProvider serviceProvider)
 {
  IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

  if (context.PrimaryEntityName != Incident.EntityLogicalName) { return; }
  if (context.MessageName != "Close") { return; }

  try
  {
   IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
   IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

   // retrieve input parameters
   Entity entityIncidentResolution = (Entity)context.InputParameters["IncidentResolution"];

   int status = ((OptionSetValue)context.InputParameters["Status"]).Value;

   // if the problem was not solved, bail
   if (status != 5) { return; }

   IncidentResolution caseResolution = entityIncidentResolution.ToEntity<IncidentResolution>();

   using (XrmServiceContext xrm = new XrmServiceContext(service))
   {
    // extract case and contract details
    ContractTemplate template =
     (
      from t in xrm.ContractTemplateSet
      join c in xrm.ContractSet on t.ContractTemplateId.Value equals c.ContractTemplateId.Id
      join i in xrm.IncidentSet on c.ContractId.Value equals i.ContractId.Id
      where i.IncidentId.Value == caseResolution.IncidentId.Id
      select t
     ).SingleOrDefault();

    if (template == null) { return; }

    // only for time based contracts
    if (template.AllotmentTypeCode == 2 /* time */)
    {
     // obtain list of all currently attached activities (Closed and NOT Billed)
     List<ActivityPointer> activities =
      (
       from act in xrm.ActivityPointerSet
       where act.RegardingObjectId.Id == caseResolution.IncidentId.Id
       && act.IsBilled == false
       && act.StateCode == 0
       select act
      ).ToList();

     // minimum 1 hr (60 mins)
     int expiredTime = activities.Sum(act => act.ActualDurationMinutes.GetValueOrDefault());

     int totalTime = Math.Max(expiredTime, 60);

     // 15 mins increment
     totalTime = 60 + (int)(Math.Ceiling((decimal)(totalTime - 60) / 15) * 15);

     int deficitTime = totalTime - expiredTime;

     if (deficitTime > 0)
     {
      // create a dummy activity that makes up the time difference
      mrcrm_timesheetactivity timesheetActivity = new mrcrm_timesheetactivity();
      timesheetActivity.ActualDurationMinutes = deficitTime;
      timesheetActivity.RegardingObjectId = new Microsoft.Xrm.Client.CrmEntityReference(Incident.EntityLogicalName, caseResolution.IncidentId.Id);
      timesheetActivity.Subject = "<DEFICIT>";
      timesheetActivity.IsBilled = false;

      timesheetActivity.Id = service.Create(timesheetActivity);

      SetStateRequest reqSetState = new SetStateRequest();
      reqSetState.EntityMoniker = timesheetActivity.ToEntityReference();
      reqSetState.State = new OptionSetValue(1); // completed
      reqSetState.Status = new OptionSetValue(2); // completed

      SetStateResponse respSetState = (SetStateResponse)service.Execute(reqSetState);

      // update case resolution time to make up the deficit
      entityIncidentResolution["timespent"] = totalTime;
     }
    }
   }
  }
  catch (FaultException<OrganizationServiceFault> ex)
  {
   throw new InvalidPluginExecutionException("An error occurred in the plug-in.", ex);
  }
 }
}

I am doing all of this in the pre-Close stage, as I need to influence the TimeSpent before the CRM has a chance to decrement allotment.

Conclusion

Not much else to say, but that one should always remember the “other” messages available to us in plugins.  Create, Update, Delete, Retrieve etc are great, but sometimes… they’re just not enough.

 

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: