123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://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
|