trabajo-afectivo/lib/escalation.rb

315 lines
8 KiB
Ruby

# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Escalation
attr_reader :ticket
def initialize(ticket, force: false)
@ticket = ticket
@force = force
end
def preferences
@preferences ||= Escalation::TicketPreferences.new(ticket)
end
def biz
@biz ||= calendar&.biz breaks: biz_breaks
end
def biz_breaks
@biz_breaks ||= Escalation::TicketBizBreak.new(ticket, calendar).biz_breaks
end
def escalation_disabled?
@escalation_disabled ||= Ticket::State.lookup(id: ticket.state_id).ignore_escalation?
end
def sla
@sla ||= Sla.for_ticket(ticket)
end
def calendar
@calendar ||= sla&.calendar
end
def forced?
!!@force
end
def force!
@force = true
end
def calculatable?
!escalation_disabled? || preferences.close_at_changed?(ticket) || preferences.last_contact_at_changed?(ticket)
end
def calculate!
calculate
ticket.save! if ticket.has_changes_to_save?
end
def calculate
if !calculatable? && !forced?
calculate_not_calculatable
elsif !calendar
calculate_no_calendar
elsif forced? || any_changes?
enforce_if_needed
update_escalations
update_statistics
apply_preferences
end
end
def any_changes?
preferences.any_changes?(ticket, sla, escalation_disabled?)
end
def assign_reset
ticket.assign_attributes(
escalation_at: nil,
first_response_escalation_at: nil,
update_escalation_at: nil,
close_escalation_at: nil
)
end
def calculate_not_calculatable
assign_reset
apply_preferences if !preferences.hash[:escalation_disabled]
end
def calculate_no_calendar
assign_reset
end
def apply_preferences
preferences.update_preferences(ticket, sla, escalation_disabled?)
end
def enforce_if_needed
return if !preferences.sla_changed?(sla) && !preferences.calendar_changed?(sla.calendar)
force!
end
def update_escalations
ticket.assign_attributes [escalation_first_response, escalation_response, escalation_update, escalation_close]
.compact
.each_with_object({}) { |elem, memo| memo.merge!(elem) }
ticket.escalation_at = calculate_next_escalation
end
def update_statistics
ticket.assign_attributes [statistics_first_response, statistics_response, statistics_update, statistics_close]
.compact
.each_with_object({}) { |elem, memo| memo.merge!(elem) }
end
private
# escalation
# skip escalation neither forced
# nor state switched from closed to open
def skip_escalation?
!forced? && !preferences.escalation_became_enabled?(escalation_disabled?)
end
def escalation_first_response
return if skip_escalation? && !preferences.first_response_at_changed?(ticket)
nullify = escalation_disabled? || ticket.first_response_at.present?
{
first_response_escalation_at: nullify ? nil : calculate_time(ticket.created_at, sla.first_response_time)
}
end
def escalation_update_reset
return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
return if sla.response_time.present? || sla.update_time.present?
{ update_escalation_at: nil }
end
def escalation_response_timestamp
return if escalation_disabled? || ticket.agent_responded?
ticket.last_contact_customer_at
end
def escalation_response
return if sla.response_time.nil?
return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
timestamp = escalation_response_timestamp
{
update_escalation_at: timestamp ? calculate_time(timestamp, sla.response_time) : nil
}
end
def escalation_update_timestamp
return if escalation_disabled?
ticket.last_contact_agent_at || ticket.created_at
end
def escalation_update
return if sla.update_time.nil?
return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
timestamp = escalation_update_timestamp
{
update_escalation_at: timestamp ? calculate_time(timestamp, sla.update_time) : nil
}
end
def escalation_close
return if skip_escalation? && !preferences.close_at_changed?(ticket)
nullify = escalation_disabled? || ticket.close_at.present?
{
close_escalation_at: nullify ? nil : calculate_time(ticket.created_at, sla.solution_time)
}
end
def calculate_time(start_time, span)
return if span.nil? || !span.positive?
Escalation::DestinationTime.new(start_time, span, biz).destination_time
end
def calculate_next_escalation
return if escalation_disabled?
[
(ticket.first_response_escalation_at if !ticket.first_response_at),
ticket.update_escalation_at,
(ticket.close_escalation_at if !ticket.close_at)
].compact.min
end
# statistics
def skip_statistics_first_response?
return true if !forced? && !preferences.first_response_at_changed?(ticket)
ticket.first_response_at.blank? || sla.first_response_time.blank?
end
def statistics_first_response
return if skip_statistics_first_response?
minutes = calculate_minutes(ticket.created_at, ticket.first_response_at)
{
first_response_in_min: minutes,
first_response_diff_in_min: minutes ? (sla.first_response_time - minutes) : nil
}
end
def skip_statistics_response?
return true if !forced? && !preferences.last_update_at_changed?(ticket)
return true if !sla.response_time
!ticket.agent_responded?
end
# ATTENTION: Recalculation after SLA change won't happen
# SLA change will cause wrong statistics in some edge cases.
# Since this changes `update_in_min` calculation to retain longest timespan.
# But it does not keep track of previous update times.
def statistics_response_applicable?(minutes)
ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
end
def statistics_response
return if skip_statistics_response?
minutes = calculate_minutes(ticket.last_contact_customer_at, ticket.last_contact_agent_at)
return if !forced? && !statistics_response_applicable?(minutes)
{
update_in_min: minutes,
update_diff_in_min: minutes ? (sla.response_time - minutes) : nil
}
end
def skip_statistics_update?
return true if !forced? && !preferences.last_update_at_changed?(ticket)
return true if !sla.update_time
ticket.last_contact_agent_at.blank?
end
# ATTENTION: Recalculation after SLA change won't happen
# SLA change will cause wrong statistics in some edge cases.
# Since this changes `update_in_min` calculation to retain longest timespan.
# But it does not keep track of previous update times.
def statistics_update_applicable?(minutes)
ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
end
def statistics_update_responses
ticket
.articles
.reverse
.lazy
.select { |article| article.sender&.name == 'Agent' && article.type&.communication }
.first(2)
end
def statistics_update_minutes
last_agent_responses = statistics_update_responses
from = last_agent_responses.second&.created_at || ticket.created_at
to = last_agent_responses.first&.created_at
calculate_minutes(from, to)
end
def statistics_update
return if skip_statistics_update?
minutes = statistics_update_minutes
return if !forced? && !statistics_update_applicable?(minutes)
{
update_in_min: minutes,
update_diff_in_min: minutes ? (sla.update_time - minutes) : nil
}
end
def skip_statistics_close?
return true if !forced? && !preferences.close_at_changed?(ticket)
ticket.close_at.blank? || sla.solution_time.blank?
end
def statistics_close
return if skip_statistics_close?
minutes = calculate_minutes(ticket.created_at, ticket.close_at)
{
close_in_min: minutes,
close_diff_in_min: minutes ? (sla.solution_time - minutes) : nil
}
end
def calculate_minutes(start_time, end_time)
return if !end_time || !start_time
Escalation::PeriodWorkingMinutes.new(start_time, end_time, ticket, biz).period_working_minutes
end
end