Pages

Thursday, October 28, 2010

Dealing with Automatic Draft Contract Expiration

I’m not sure how much of this situation Microsoft is aware, but I have long known about a behavior of the “Update Contract States” job that only recently became a problem for me and my employer:

Contracts and Contract Lines in Draft state will automatically expire when the “End Date” occurs in the past.

You read that right:  Draft state.  I could speculate as to why the system behaves this way, but for now I’m just going to assume it’s a bug and soon write-up a report back to Microsoft about it.  However, I cannot wait for them to address it, and must stop our Draft Contracts from entering into an Expired state.  Here’s a few business-cases in which I find this behavior obstructive:

Entering historical data:

Obviously, I ultimately want historical data regarding Contracts to ultimately progress to the Expired state.  Unfortunately, because the job runs once a day, at 3pm, it’s become a race to finalize the record and make sure it is perfect before the arbitrary deadline smacks the record with the Expired-stick.  Sure, I could alter the frequency, or probably even the execution time of the job—but those efforts neither eliminate the deadline, nor treat the desirable state transitions acceptably.

Using Drafts to produce a history for “lost” service opportunities:

Call me crazy, but I find that the Contract deals with certain things better than a Quote does, so my first task in providing a comprehensive Sales quoting utility was to establish a custom relationship between the Opportunity and the Contract entities.  It was either that, or copying the Contract’s mechanisms within the Quote and Quote Product (and I am loathe to duplicate functionality or reinvent wheels).

We determined at the time that when an Opportunity was lost, we would retain the Draft Contracts as a historical accounting of the various service options that had been quoted to the customer.  The problem is that now the “End Dates” on some of these early records are starting to fall into the past, and the records appear to be “Expired” along with any other legitimately historical record.  Ideally, I’d love to have an alternate state to use for these records, but I haven’t had the time to bother with approaching that situation—instead preferring to leave them in a “Draft” state indefinitely.

Conclusion:

To preserve the accuracy of the representations made by the state of a Contract record, I need a way to prevent “Drafts” from progressing automatically into the “Expired” state.  The obvious, and easy choice, is to go with a Plug-in that attaches to the SetState and SetStateDynamicEntity messages for both the Contract and Contract Line entities.

I use two code files to define state-change handlers for each entity, and a register.xml file to deploy the Plug-in with the CRM Plug-in Developer Tool.  This is helpful because there are Pre-stage entity images that get used by the code to identify the previous state of the record (I don’t want to accidentally block the progress of a traditionally-Invoiced Contract).

Here are the code files in use for my CrmFixes project:

ContractSetStateHandler.cs:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace CrmFixes
{
  /// <summary>
  /// Executes before the SetState or SetStateDynamicEntity of a Contract in CRM
  /// </summary>
  public class ContractSetStateHandler : IPlugin
  {
    #region IPlugin Members

    /// <summary>
    /// Checks the advancement of state for the Contract record, and throws an exception to
    /// prevent the automatic advancement of a Contract in Draft status to Expired status.
    /// </summary>
    /// <param name="context">Context during execution of CRM Plugin</param>
    public void Execute(IPluginExecutionContext context)
    {
      // Check the InputParameters for an EntityMoniker instance to work with; validate type
      if (context.InputParameters.Contains("EntityMoniker") && context.InputParameters["EntityMoniker"] is Moniker)
      {
        Moniker contractInfo = (Moniker)context.InputParameters["EntityMoniker"];

        // Verify entity is a Contract instance
        if (contractInfo.Name != EntityName.contract.ToString())
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Not a Contract instance.");

        if (!context.InputParameters.Contains("state"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing state in context.");

        if (!context.PreEntityImages.Contains("PreSetStateContract"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing PreSetStateContract image.");

        String newStateCode = (String)context.InputParameters["state"];
        DynamicEntity preSetStateContract = (DynamicEntity)context.PreEntityImages["PreSetStateContract"];

        if (!preSetStateContract.Properties.Contains("statecode"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing statecode in image.");

        String oldStateCode = (String)preSetStateContract.Properties["statecode"];

        // Make sure we only care about a transition from Draft to Expired
        if (newStateCode == "Expired" && oldStateCode == "Draft")
          throw new InvalidPluginExecutionException("Draft Contracts are not allowed to automatically expire.");
      }
      else
        throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Expected EntityMoniker unavailable.");
    }

    #endregion
  }
}
ContractDetailSetStateHandler.cs:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace CrmFixes
{
  /// <summary>
  /// Executes before the SetState or SetStateDynamicEntity of a Contract in CRM
  /// </summary>
  public class ContractDetailSetStateHandler : IPlugin
  {
    #region IPlugin Members

    /// <summary>
    /// Checks the advancement of state for the Contract record, and throws an exception to
    /// prevent the automatic advancement of a Contract in Draft status to Expired status.
    /// </summary>
    /// <param name="context">Context during execution of CRM Plugin</param>
    public void Execute(IPluginExecutionContext context)
    {
      // Check the InputParameters for an EntityMoniker instance to work with; validate type
      if (context.InputParameters.Contains("EntityMoniker") && context.InputParameters["EntityMoniker"] is Moniker)
      {
        Moniker contractInfo = (Moniker)context.InputParameters["EntityMoniker"];

        // Verify entity is a Contract instance
        if (contractInfo.Name != EntityName.contractdetail.ToString())
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Not a Contract Detail instance.");

        if (!context.InputParameters.Contains("state"))
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Missing state in context.");

        if (!context.PreEntityImages.Contains("PreSetStateContractDetail"))
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Missing PreSetStateContractDetail image.");

        String newStateCode = (String)context.InputParameters["state"];
        DynamicEntity preSetStateContractDetail = (DynamicEntity)context.PreEntityImages["PreSetStateContractDetail"];

        Lookup contractId = (Lookup)preSetStateContractDetail.Properties["contractid"];

        ICrmService crmService = context.CreateCrmService(false);

        contract contract = (contract)crmService.Retrieve(EntityName.contract.ToString(), contractId.Value, 
          new ColumnSet(new string[] { "contractid", "statecode" }));

        // Make sure we only care about a transition from Draft to Expired
        if (newStateCode == "Expired" && contract.statecode.Value == ContractState.Draft)
          throw new InvalidPluginExecutionException("Contract Details on draft Contracts are not allowed to automatically expire.");
      }
      else
        throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Expected EntityMoniker unavailable.");
    }

    #endregion
  }
}
register.xml:
<?xml version="1.0" encoding="utf-8" ?>
<!-- Input file for the PluginDeveloper tool that provides the configuration for
     registering a workflow activity or plug-in. -->

<Register
    LogFile = "Plug-in Registration Log.txt"
    Server  = "http://crm"
    Org     = "Org"
    Domain  = "domain"
    UserName= "user" >
  <Solution SourceType="0" Assembly="path-to\CrmFixes.dll">
    <Steps>
      <Step
        CustomConfiguration = ""
        Description = "Draft Contract expiration denial : SetState Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetState"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Expiration Handler"
        PluginTypeName = "CrmFixes.ContractSetStateHandler"
        PrimaryEntityName = "contract"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContract" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractid,statecode" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract expiration denial : SetStateDynamicEntity Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetStateDynamicEntity"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Expiration Handler"
        PluginTypeName = "CrmFixes.ContractSetStateHandler"
        PrimaryEntityName = "contract"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContract" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractid,statecode" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract Detail expiration denial : SetState Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetState"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Detail Expiration Handler"
        PluginTypeName = "CrmFixes.ContractDetailSetStateHandler"
        PrimaryEntityName = "contractdetail"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContractDetail" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractdetailid,contractid" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract Detail expiration denial : SetStateDynamicEntity Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetStateDynamicEntity"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Detail Expiration Handler"
        PluginTypeName = "CrmFixes.ContractDetailSetStateHandler"
        PrimaryEntityName = "contractdetail"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContractDetail" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractdetailid,contractid" />
        </Images>
      </Step>
    </Steps>
  </Solution>
</Register>