Develop your own plugins for SLA calculation!

Purpose

The purpose of a ticket SLA plugin is to start / stop measuring the SLA

Setup

Add references to:

  • Netadmin.Management.dll
  • Netadmin.Core.dll
  • Netadmin.Management.Interfaces.Ticketing.SLA.dll
Implement ITicketSlaPlugin 
Implementation
The interface requires implementation of several read only properties
string Name - The name of the plugin
string Description - A description for the plugin
string SlaTypeName - The name of the SLA TYPE the plugin handles
Guid UniqueIdentifier - A globally unique identifier for this specific plugin.
The UniqueIdentifier must always be constant and unique for the plugin
 

The Method

Execute

The only parameter is an IEnumerable of ITicketSlaDetails

Each ITicketSlaDetail instance will be pre-populated with ticket related information.

TicketDetails - Details for the ticket itself, including the ticket id
Posts - Posts in the ticket
LastPost - The most recent post in the ticket
Types - A dictionary containing all ticket types relevant to this ticket and post.
StatusesA dictionary containing all ticket statuses relevant to this ticket and post.
Departments - A dictionary containing all departments relevant to this ticket and post.
Priorities - A dictionary containing all ticket priorities relevant to this ticket and post.

UsersA dictionary containing all ticket users relevant to this ticket and post.

ExistingRecords - All existing records related to this ticket
FrozenRecords - All frozen records related to this ticket
OfficeHours - The settings for office hours
Holidays - The settings for Holidays

 

The execute method is called when

  • A new post is added to a ticket
  • Time is frozen or unfrozen
  • A user clicks the execute button in the UI

The plugin should, based on the input,  start and/or stop sla measurement. It should always return the closest deadline in every call. An injected ITicketSlaManager is very helpful here.

In general the plugin must consider

  • If the sla is currently active or not
  • If something is triggering starting/stopping measurement. 
  • If closing sla, it needs to decide if the time was breached, and if so, by how much.
  • When it calculates time it must consider if the ticket is frozen, the office hours and holidays. ITicketSlaManager can help there
  • Always return an accurate deadline, if one exists 

Note that the plugin may be asked to process multiple tickets in one call and should be capable of doing so in an efficient manner.

 

Best practices

General

  • When pausing sla for any reason, send null as breachTime to the StopSla method. This makes it easier to distinguish between records that marks a breach versus records that tracks time between pauses.
  • Ensure that you're only looking at relevant records. TicketDetails.ExistingRecords contains records for all sla types, including frozen records (slatype id 0)

 

Finalized sla

Depending on what type of sla the plugin is tracking, it may be useful to know if it further processing is needed. For example a CLOSED type is only done once. If following these best practices, it is possible to check if there are existing finalized records.

One way of doing this is to check if there are any records with our sla type that has a EndTime and a breach value. If there is one, this type has already been done once on this ticket and further processing of this ticket can be skipped.

 

//We're only interested in records for our own sla type
var relevantRecords = ticketDetails.ExistingRecords.Where(x => x.TypeId == type.Id).ToList();

//If there is at least one record with a breachtime value, this sla type is done
var isDone = relevantRecords.Any(x => x.EndTime.HasValue && x.BreachTime.HasValue);
if (isDone) [...];

​ 

Frozen tickets

 To determine if a ticket is frozen the plugin can call the IsFrozen() method on ITicketSlaManager

var isFrozen = _slaManager.IsFrozen(ticketDetails.TicketDetails.Id, ticketDetails.FrozenRecords);

 

It is up to the plugin to pause / resume sla time when the ticket is frozen / unfrozen. Typically you do this by calling StopSla to pause and StartSla to resume on a ITicketSlaManager. Note that null is passed as breachtime when we pause sla.

When resuming, it is important to supply the new deadline to StartSla. This deadline should also be returned from the execute method

//Pause
_slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, null);

//Resume
_slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);

 

 

Active

To determine is a sla is active for a specific type you can use 

var isActive = _slaManager.IsTypeActive(type, ticketDetails.ExistingRecords);

 

If the type is not active, the plugin need to determine if something is triggering the start of measurement. To start measurement use 

_slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);

 

If the type has been active before you may want to take into account previous active periods when calculating the deadline. Remember to ensure that the deadline is returned from the plugin. See below for more details on how to perform time calculations

 

If the type is active the plugin need to determine if measurement should be stopped, and if it should, if it the deadline was breached or not. To stop sla use

_slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);

In most cases you do not want to record a negative breach time, but it remains possible for complex sla type scenarios

 

Breach time

  • Negative breach time means no breach. Positive breach time means breach, that is the SLA was not met.
  • If breach time is negative – you may want to set breach time to zero (best practice)
  • Always set the final breach time if positive (final as in when SLA type is stopped definitely, e.g. ticket is being closed)
    • SLA that measures one time on the ticket, e.g. CLOSED, should have one an only one record with a breach time set
    • SLA that may measure several times on a ticket e.g. "UPDATE REQUIRED", should have breach time set on every record that marks an end

 

Time Calculation

There are a couple of very useful methods in ITicketSlaManager that aids in calculating deadlines and spent time

GetTotalTime(type, records, officeHours, holidays, endTime);

This method sums all time in records, but only during office hours and not during holidays.

  • type - The sla type the plugin handles
  • records - The records to consider. These will be filtered by the type
  • officeHours - Unless there are special office hours for this type, these can be found in the ticketDetails passed to the plugin
  • holidays - Unless there are special holidays for this type, these can be found in the ticketDetails passed to the plugin
  • endTime - This is the time used when calculating on open records (without end time). Usually the current time, but can be any time

 

GetEndTime(startTime, remainingTime, officeHours, holidays)

This metod calculates an end time, usually a deadline, based on a starting time, but only includes office hours and excludes holidays

  • startTime - Starting point for the calculation. Usually current time but can be anything, like the creation time of a ticket
  • remainingTime - How much time should be added to the startTime, usually the deadline minus any time already spent
  • officeHours - Unless there are special office hours for this type, these can be found in the ticketDetails passed to the plugin
  • holidays - Unless there are special holidays for this type, these can be found in the ticketDetails passed to the plugin

 

Example for calculating a deadline:

var currentTime = _dateTimeProvider.Now();

//Only records for this type
var relevantRecords = ticketDetails.ExistingRecords.Where(x => x.TypeId == type.Id).ToList();

//Only records since the last breach
var recordsForThisCycle = relevantRecords.OrderByDescending(x => x.StartTime).TakeWhile(x => x.BreachTime == null);

//Calculate the sum of all spent time this cycle
var spentTime = _slaManager.GetTotalTime(type, recordsForThisCycle, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);

//Calculate the remaining time based on the deadline configured on the sla type
var remainingTime = TimeSpan.FromHours(type.Deadline) - spentTime;

//Calculate the deadline based on remaining time
var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);

//Start sla with the deadline
_slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);

 

Sla plugins and unit testing

Avoid using DateTime.Now as it is not mockable. As you will see in the example plugins below, a IDateTimeProvider is injected in the plugin constructor. Runtime this will be injected by an instance that calls DateTime.Now, but during testing you can supply something that allows you to use any DateTime as current time. This allows you to write reliable tests that does not depend on the current time.

 

Edge Case

There is an easy to miss edge case when writing your plugins. What happens if the condition to stop measurement is met while the ticket is frozen? In our example plugins we handle this by resuming sla, followed by immediately ending with a breach that equals the breach that existed when the ticket was paused.

 

Example plugins

Assigned Within

    /// <summary>
    /// Measures the time for tickets being assigned to any individual
    /// </summary>
    public class AssignedWithin : ITicketSlaPlugin
    {
        //This Guid MUST be unique, no copy-paste allowed
        private static readonly Guid UniqueId = new Guid("[YOUR GUID HERE]");

        private readonly ISlaTypeManager _slaTypeManager;
        private readonly ITicketSlaManager _slaManager;
        private readonly ISystemLogger _logger;
        private readonly IDateTimeProvider _dateTimeProvider;

        public string Name => "ASSIGNED WITHIN";
        public string Description => "Measures the time for tickets being assigned to any individual";
        public string SlaTypeName => Name;
        public Guid UniqueIdentifier => UniqueId;

        public AssignedWithin(ISlaTypeManager slaTypeManager
            , ITicketSlaManager slaManager
            , ISystemLogger logger                  //NLog log wrapper
            , IDateTimeProvider dateTimeProvider    //Useful for unit testing
            )
        {
            _slaTypeManager = slaTypeManager;
            _slaManager = slaManager;
            _logger = logger;
            _dateTimeProvider = dateTimeProvider;
        }

        private bool ShouldStartSla(ITicketSlaDetails ticketSlaDetails)
        {
            return ticketSlaDetails.Posts.Count() == 1 ||!ticketSlaDetails.Posts.Any(x => x.OwnerId > 0);
        }

        private bool ShouldEndSla(ITicketSlaDetails ticketSlaDetails)
        {
            return ticketSlaDetails.Posts.Any(x => x.OwnerId > 0) || ticketSlaDetails.TicketDetails.Closed;
        }

        public IDictionary<long, DateTime> Execute(IEnumerable<ITicketSlaDetails> tickets)
        {
            //Get the matching SLA type
            var type = _slaTypeManager.GetAnyByNames(new[] { Name }).Values.FirstOrDefault(x => x.Name == Name);

            //If there is no match, this is a plugin for a SLA type that does not exist in Netadmin
            if (type == null) return new Dictionary<long, DateTime>();

            var resultDictionary = new Dictionary<long, DateTime>();
            foreach (var ticketDetails in tickets)
            {
                //We're only interested in records for our own sla type
                var relevantRecords = ticketDetails.ExistingRecords.Where(x => x.TypeId == type.Id).ToList();

                //If there is at least one record with a breachtime value, this sla type is done
                var isDone = relevantRecords.Any(x => x.EndTime.HasValue && x.BreachTime.HasValue);
                if (isDone) continue;

                var isActive = _slaManager.IsTypeActive(type, relevantRecords);
                var isFrozen = _slaManager.IsFrozen(ticketDetails.TicketDetails.Id, ticketDetails.FrozenRecords);

                if (isActive)
                {
                    if (ShouldEndSla(ticketDetails))
                    {
                        //Stop counting SLA when ticket is assigned or closed
                        var endTime = _dateTimeProvider.Now();
                        var totalTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var breachTime =  totalTime - TimeSpan.FromHours(type.Deadline);

                        _logger.Debug("{0}: Ticket {1} - Stopping SLA as assigned with final endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, breachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);
                        continue;
                    }

                    if (isFrozen)
                    {
                        //SLA is active, but ticket just got frozen. Need to pause SLA
                        var endTime = _dateTimeProvider.Now();
                        var deadlineTime = TimeSpan.FromHours(type.Deadline);
                        var totalTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var potentialBreachTime = totalTime - deadlineTime;

                        _logger.Debug("{0}: Ticket {1} - Pausing SLA with endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, potentialBreachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, null);
                        continue;
                    }

                    //Ticket is not frozen
                    var currentTime = _dateTimeProvider.Now();
                    var currentDeadLine = relevantRecords.FirstOrDefault(a => a.EndTime == null)?.Deadline;

                    //Compare current active deadline with the calculated one to see if something has been changed
                    //How much time has been spent on this ticket until now and how much is left
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var remainingTime = TimeSpan.FromHours(type.Deadline) - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);

                    if (deadLine == currentDeadLine)
                    {
                        //Do nothing
                        _logger.Debug("{0}: Ticket {1} - Doing nothing because SLA is currently doing its work", Name, ticketDetails.TicketDetails.Id);
                    }
                    else
                    {
                        //Fix this SLA type since the deadline is not correct anymore
                        _logger.Debug("{0}: Ticket {1} - Fixing SLA with old deadline {2} vs new deadline {3}", Name, ticketDetails.TicketDetails.Id, currentDeadLine, deadLine);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, null);
                        _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);
                    }
                    resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                    continue;
                }

                if (ShouldStartSla(ticketDetails))
                {
                    var currentTime = _dateTimeProvider.Now();
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var deadlineTime = TimeSpan.FromHours(type.Deadline);

                    var remainingTime = deadlineTime - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);
                    _logger.Debug("{0}: Ticket {1} - Starting SLA with deadline {2}", Name, ticketDetails.TicketDetails.Id, deadLine);
                    _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);
                    if (ShouldEndSla(ticketDetails))
                    {
                        //If ticket is created assigned then immediatly stop SLA
                        _logger.Debug("{0}: Ticket {1} - Stopping SLA, was assigned during creation", Name, ticketDetails.TicketDetails.Id, deadLine);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, TimeSpan.Zero);
                        continue;
                    }
                    if (!isFrozen)
                    {
                        resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                    }
                    continue;
                }

                //Edge case where SLA is fulfilled while ticket is frozen
                if (isFrozen && ShouldEndSla(ticketDetails))
                {
                    var currentTime = _dateTimeProvider.Now();
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var deadlineTime = TimeSpan.FromHours(type.Deadline);
                    var breachTime = spentTime - deadlineTime;
                    _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, null);
                    _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);
                    continue;
                }
            }
            return resultDictionary;
        }
    }

 

 

Closed

    /// <summary>
    /// Measures the time for tickets to closure (closed status)
    /// </summary>
    public class Closed : ITicketSlaPlugin
    {
        private static readonly Guid UniqueId = new Guid("YOUR GUID HERE");

        private readonly ISlaTypeManager _slaTypeManager;
        private readonly ITicketSlaManager _slaManager;
        private readonly ISystemLogger _logger;
        private readonly IDateTimeProvider _dateTimeProvider;
        public string Name => "CLOSED";
        public string Description => "Measures the time for tickets to closure (closed status)";
        public string SlaTypeName => Name;
        public Guid UniqueIdentifier => UniqueId;

        //Standard Netadmin IoC constructor injection applied
        public Closed(ISlaTypeManager slaTypeManager
            , ITicketSlaManager slaManager
            , ISystemLogger logger                      //NLog log wrapper
            , IDateTimeProvider dateTimeProvider        //Useful for unit testing
            )
        {
            _slaTypeManager = slaTypeManager;
            _slaManager = slaManager;
            _logger = logger;
            _dateTimeProvider = dateTimeProvider;
        }

        private bool ShouldStartSla(ITicketSlaDetails ticketSlaDetails)
        {
            return !ShouldEndSla(ticketSlaDetails);
        }
        private bool ShouldEndSla(ITicketSlaDetails ticketSlaDetails)
        {
            return ticketSlaDetails.TicketDetails.Closed;
        }

        public IDictionary<long, DateTime> Execute(IEnumerable<ITicketSlaDetails> tickets)
        {
            //Get the matching SLA type
            var type = _slaTypeManager.GetAnyByNames(new[] { Name }).Values.FirstOrDefault(x => x.Name == Name);

            //If there is no match, this is a plugin for a SLA type that does not exist in Netadmin
            if (type == null) return new Dictionary<long, DateTime>();

            var resultDictionary = new Dictionary<long, DateTime>();
            foreach (var ticketDetails in tickets)
            {
                //We're only interested in records for our own sla type
                var relevantRecords = ticketDetails.ExistingRecords.Where(x => x.TypeId == type.Id).ToList();

                //If there is at least one record with a breachtime value, this sla type is done
                var isDone = relevantRecords.Any(x => x.EndTime.HasValue && x.BreachTime.HasValue);
                if (isDone) continue;

                var isActive = _slaManager.IsTypeActive(type, relevantRecords);
                var isFrozen = _slaManager.IsFrozen(ticketDetails.TicketDetails.Id, ticketDetails.FrozenRecords);


                //Check if we're currently counting SLA
                if (isActive)
                {
                    if (ShouldEndSla(ticketDetails))
                    {
                        //Stop counting SLA when ticket is closed
                        var endTime = ticketDetails.TicketDetails.LastUpdatedDate;
                        var totalTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var breachTime = totalTime - TimeSpan.FromHours(type.Deadline);

                        _logger.Debug("{0}: Ticket {1} - Stopping SLA as assigned with final endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, breachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);
                        continue;
                    }

                    if (isFrozen)
                    {
                        //SLA is active, but ticket just got frozen. Need to pause SLA

                        var endTime = _dateTimeProvider.Now();
                        var totalTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var potentialBreachTime = totalTime - TimeSpan.FromHours(type.Deadline);

                        _logger.Debug("{0}: Ticket {1} - Pausing SLA with endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, potentialBreachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, null);
                        continue;
                    }

                    var currentTime = _dateTimeProvider.Now();
                    var currentDeadLine = ticketDetails.ExistingRecords.FirstOrDefault(a => a.EndTime == null)?.Deadline;

                    //Compare current active deadline with the calculated one to see if something has been changed
                    //How much time has been spent on this ticket until now and how much is left
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var remainingTime = TimeSpan.FromHours(type.Deadline) - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);

                    if (deadLine == currentDeadLine)
                    {
                        //Do nothing
                        _logger.Debug("{0}: Ticket {1} - Doing nothing because SLA is currently doing its work", Name, ticketDetails.TicketDetails.Id);
                    }
                    else
                    {
                        //Fix this SLA type since the deadline is not correct anymore
                        _logger.Debug("{0}: Ticket {1} - Fixing SLA with old deadline {2} vs new deadline {3}", Name, ticketDetails.TicketDetails.Id, currentDeadLine, deadLine);

                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, null);
                        _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);
                    }
                    resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                    continue;
                }

                if (ShouldStartSla(ticketDetails))
                {
                    var currentTime = _dateTimeProvider.Now();
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var remainingTime = TimeSpan.FromHours(type.Deadline) - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);
                    _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);

                    var action = remainingTime == TimeSpan.FromHours(type.Deadline) ? "Starting" : "Resuming";
                    _logger.Debug("{0}: Ticket {1} - {2} SLA with deadline {3}, remaining time {4}", Name, ticketDetails.TicketDetails.Id, action, deadLine, remainingTime);

                    resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                }

                //Edge case where SLA is fulfilled while ticket is frozen
                if (isFrozen && ShouldEndSla(ticketDetails))
                {
                    var currentTime = _dateTimeProvider.Now();
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var deadlineTime = TimeSpan.FromHours(type.Deadline);
                    var breachTime = spentTime - deadlineTime;
                    _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, null);
                    _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);
                    continue;
                }
            }
            return resultDictionary;
        }
    }

 

Initial Response

    /// <summary>
    /// Measures the initial response for tickets created by external users
    /// </summary>
    public class InitialResponse : ITicketSlaPlugin
    {
        //This Guid MUST be unique, no copy-paste allowed
        private static readonly Guid UniqueId = new Guid("YOUR GUID HERE");

        private readonly ISlaTypeManager _slaTypeManager;
        private readonly ITicketSlaManager _slaManager;
        private readonly ISystemLogger _logger;
        private readonly IDateTimeProvider _dateTimeProvider;
        public string Name => "INITIAL RESPONSE";
        public string Description => "Measures the initial response for tickets created by external users";
        public string SlaTypeName => Name;
        public Guid UniqueIdentifier => UniqueId;

        public InitialResponse(ISlaTypeManager slaTypeManager
            , ITicketSlaManager slaManager
            , ISystemLogger logger                  //NLog log wrapper
            , IDateTimeProvider dateTimeProvider    //Useful for unit testing
            )
        {
            _slaTypeManager = slaTypeManager;
            _slaManager = slaManager;
            _logger = logger;
            _dateTimeProvider = dateTimeProvider;
        }

        private bool ShouldStartSla(ITicketSlaDetails ticketSlaDetails)
        {
            return ticketSlaDetails.Posts.Count() == 1
                   && ticketSlaDetails.Users.ContainsKey(ticketSlaDetails.LastPost.AuthorId)
                   && (ticketSlaDetails.Users[ticketSlaDetails.LastPost.AuthorId].AdapterName == AdapterConstants.ExternalAdapterName
                       || ticketSlaDetails.Users[ticketSlaDetails.LastPost.AuthorId].AdapterName == AdapterConstants.CustomerAdapterName);
        }
        private bool ShouldEndSla(ITicketSlaDetails ticketSlaDetails)
        {
            return ticketSlaDetails.TicketDetails.Closed ||
                   ticketSlaDetails.Users.ContainsKey(ticketSlaDetails.LastPost.AuthorId)
                   && ticketSlaDetails.Users[ticketSlaDetails.LastPost.AuthorId].AdapterName == AdapterConstants.NaAdapterName;
        }

        public IDictionary<long, DateTime> Execute(IEnumerable<ITicketSlaDetails> tickets)
        {
            //Get the matching SLA type
            var type = _slaTypeManager.GetAnyByNames(new[] { Name }).Values.FirstOrDefault(x => x.Name == Name);

            //If there is no match, this is a plugin for a SLA type that does not exist in Netadmin
            if (type == null) return new Dictionary<long, DateTime>();

            var resultDictionary = new Dictionary<long, DateTime>();
            foreach (var ticketDetails in tickets)
            {
                //We're only interested in records for our own sla type
                var relevantRecords = ticketDetails.ExistingRecords.Where(x => x.TypeId == type.Id).ToList();

                //If there is at least one record with a breachtime value, this sla type is done
                var isDone = relevantRecords.Any(x => x.EndTime.HasValue && x.BreachTime.HasValue);
                if (isDone) continue;

                var isActive = _slaManager.IsTypeActive(type, relevantRecords);
                var isFrozen = _slaManager.IsFrozen(ticketDetails.TicketDetails.Id, ticketDetails.FrozenRecords);

                if (isActive)
                {
                    //Stop counting sla when ticket is closed or has a post by a NA user in it
                    if (ShouldEndSla(ticketDetails))
                    {
                        var endTime = _dateTimeProvider.Now();
                        var totalTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var breachTime = totalTime - TimeSpan.FromHours(type.Deadline);
                        _logger.Debug("{0}: Ticket {1} - Stopping SLA as assigned with final endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, breachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);
                        continue;
                    }

                    if (isFrozen)
                    {
                        //SLA is active, but ticket just got frozen. Need to pause SLA

                        var endTime = _dateTimeProvider.Now();
                        var deadlineTime = TimeSpan.FromHours(type.Deadline);
                        var totalTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var potentialBreachTime = totalTime - deadlineTime;

                        _logger.Debug("{0}: Ticket {1} - Pausing SLA with endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, potentialBreachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, null);
                        continue;
                    }

                    //Ticket is not frozen
                    var currentTime = _dateTimeProvider.Now();
                    var currentDeadLine = relevantRecords.FirstOrDefault(a => a.EndTime == null)?.Deadline;

                    //Compare current active deadline with the calculated one to see if something has been changed
                    //How much time has been spent on this ticket until now and how much is left
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var remainingTime = TimeSpan.FromHours(type.Deadline) - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);

                    if (deadLine == currentDeadLine)
                    {
                        //Do nothing
                        _logger.Debug("{0}: Ticket {1} - Doing nothing because SLA is currently doing its work", Name, ticketDetails.TicketDetails.Id);
                    }
                    else
                    {
                        //Fix this SLA type since the deadline is not correct anymore
                        _logger.Debug("{0}: Ticket {1} - Fixing SLA with old deadline {2} vs new deadline {3}", Name, ticketDetails.TicketDetails.Id, currentDeadLine, deadLine);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, null);
                        _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);
                    }
                    resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                    continue;
                }

                if (ShouldStartSla(ticketDetails))
                {
                    var currentTime = _dateTimeProvider.Now();
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var deadlineTime = TimeSpan.FromHours(type.Deadline);

                    var remainingTime = deadlineTime - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);
                    _logger.Debug("{0}: Ticket {1} - Starting SLA with deadline {2}", Name, ticketDetails.TicketDetails.Id, deadLine);
                    _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);
                    if (!isFrozen)
                    {
                        resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                    }
                    continue;
                }

                //Edge case where SLA is fulfilled while ticket is frozen
                if (isFrozen && ShouldEndSla(ticketDetails))
                {
                    var currentTime = _dateTimeProvider.Now();
                    var spentTime = _slaManager.GetTotalTime(type, ticketDetails.ExistingRecords, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var deadlineTime = TimeSpan.FromHours(type.Deadline);
                    var breachTime = spentTime - deadlineTime;
                    _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, null);
                    _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);
                    continue;
                }
            }
            return resultDictionary;
        }
    }

 

Update Required

    /// <summary>
    /// Measures and keeps track of tickets that needs to be updated periodically
    /// </summary>
    public class UpdateRequired : ITicketSlaPlugin
    {
        private static readonly Guid UniqueId = new Guid("YOUR GUID HERE");

        private readonly ISlaTypeManager _slaTypeManager;
        private readonly ITicketSlaManager _slaManager;
        private readonly ISystemLogger _logger;
        private readonly IDateTimeProvider _dateTimeProvider;
        public string Name => "UPDATE REQUIRED";
        public string Description => "Measures and keeps track of tickets that needs to be updated periodically";
        public string SlaTypeName => Name;
        public Guid UniqueIdentifier => UniqueId;

        public UpdateRequired(ISlaTypeManager slaTypeManager
            , ITicketSlaManager slaManager
            , ISystemLogger logger                      //NLog log wrapper
            , IDateTimeProvider dateTimeProvider        //Useful for unit testing
            )
        {
            _slaTypeManager = slaTypeManager;
            _slaManager = slaManager;
            _logger = logger;
            _dateTimeProvider = dateTimeProvider;
        }

        private bool ShouldStartSla(ITicketSlaDetails ticketSlaDetails)
        {
            return !ticketSlaDetails.TicketDetails.Closed;
        }
        private bool ShouldEndSla(ITicketSlaDetails ticketSlaDetails)
        {
            return ticketSlaDetails.TicketDetails.Closed 
                || (ticketSlaDetails.Users.ContainsKey(ticketSlaDetails.LastPost.AuthorId)
                   && ticketSlaDetails.Users[ticketSlaDetails.LastPost.AuthorId].AdapterName == AdapterConstants.NaAdapterName);
        }

        public IDictionary<long, DateTime> Execute(IEnumerable<ITicketSlaDetails> tickets)
        {
            var type = _slaTypeManager.GetAnyByNames(new[] { Name }).Values.FirstOrDefault(x => x.Name == Name);
            if (type == null) return new Dictionary<long, DateTime>();

            var resultDictionary = new Dictionary<long, DateTime>();
            foreach (var ticketDetails in tickets)
            {
                var relevantRecords = ticketDetails.ExistingRecords.Where(x => x.TypeId == type.Id).ToList();
                var recordsForThisCycle = relevantRecords.OrderByDescending(x => x.StartTime).TakeWhile(x => x.BreachTime == null);

                var isActive = _slaManager.IsTypeActive(type, relevantRecords);
                var isFrozen = _slaManager.IsFrozen(ticketDetails.TicketDetails.Id, ticketDetails.FrozenRecords);

                //If SLA is currently being measured
                if (isActive)
                {
                    //If the last post is made by a NA user
                    if (ShouldEndSla(ticketDetails))
                    {
                        var endTime = _dateTimeProvider.Now();
                        var totalTime = _slaManager.GetTotalTime(type, recordsForThisCycle, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var breachTime = totalTime - TimeSpan.FromHours(type.Deadline);

                        _logger.Debug("{0}: Ticket {1} - Stopping SLA as assigned with final endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, breachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, breachTime > TimeSpan.Zero ? breachTime : TimeSpan.Zero);

                        //Start sla again?
                        if (ShouldStartSla(ticketDetails))
                        {
                            var newDeadline = _slaManager.GetEndTime(endTime, TimeSpan.FromHours(type.Deadline), ticketDetails.OfficeHours, ticketDetails.Holidays);
                            _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, endTime, newDeadline);
                            resultDictionary.Add(ticketDetails.TicketDetails.Id, newDeadline);
                        }
                        continue;
                    }

                    if (isFrozen)
                    {
                        //SLA is active, but ticket just got frozen. Need to pause SLA
                        var endTime = _dateTimeProvider.Now();
                        var totalTime = _slaManager.GetTotalTime(type, recordsForThisCycle, ticketDetails.OfficeHours, ticketDetails.Holidays, endTime);
                        var potentialBreachTime = totalTime - TimeSpan.FromHours(type.Deadline);

                        _logger.Debug("{0}: Ticket {1} - Pausing SLA with endtime {2}, breachtime {3}", Name, ticketDetails.TicketDetails.Id, endTime, potentialBreachTime);
                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, endTime, null);
                        continue;
                    }

                    var currentTime = _dateTimeProvider.Now();
                    var currentDeadLine = ticketDetails.ExistingRecords.FirstOrDefault(a => a.EndTime == null)?.Deadline;

                    //Compare current active deadline with the calculated one to see if something has been changed
                    //How much time has been spent on this ticket until now and how much is left
                    var spentTime = _slaManager.GetTotalTime(type, recordsForThisCycle, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var remainingTime = TimeSpan.FromHours(type.Deadline) - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);

                    if (deadLine == currentDeadLine)
                    {
                        //Do nothing
                        _logger.Debug("{0}: Ticket {1} - Doing nothing because SLA is currently doing its work", Name, ticketDetails.TicketDetails.Id);
                    }
                    else
                    {
                        //Fix this SLA type since the deadline is not correct anymore
                        _logger.Debug("{0}: Ticket {1} - Fixing SLA with old deadline {2} vs new deadline {3}", Name, ticketDetails.TicketDetails.Id, currentDeadLine, deadLine);

                        _slaManager.StopSla(type, ticketDetails.TicketDetails.Id, currentTime, null);
                        _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);
                    }
                    resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                    continue;
                }

                if (!isFrozen && ShouldStartSla(ticketDetails))
                {
                    var currentTime = _dateTimeProvider.Now();
                    var spentTime = _slaManager.GetTotalTime(type, recordsForThisCycle, ticketDetails.OfficeHours, ticketDetails.Holidays, currentTime);
                    var remainingTime = TimeSpan.FromHours(type.Deadline) - spentTime;

                    var deadLine = _slaManager.GetEndTime(currentTime, remainingTime, ticketDetails.OfficeHours, ticketDetails.Holidays);
                    _slaManager.StartSla(type, ticketDetails.TicketDetails.Id, currentTime, deadLine);

                    var action = remainingTime == TimeSpan.FromHours(type.Deadline) ? "Starting" : "Resuming";
                    _logger.Debug("{0}: Ticket {1} - {2} SLA with deadline {3}, remaining time {4}", Name, ticketDetails.TicketDetails.Id, action, deadLine, remainingTime);

                    resultDictionary.Add(ticketDetails.TicketDetails.Id, deadLine);
                }
            }
            return resultDictionary;
        }
    }

Note! This plugin requires Custom IoC Registration!
Plugin Details
Ticket SLA Plugin
Plugin
ITicketSlaPlugin