Ticket SLA Plugin
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
string SlaTypeName - The name of the SLA TYPE the plugin handles
Guid UniqueIdentifier - A globally unique identifier for this specific 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.
Statuses - A 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.
Users - A 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; } }