escalation.rb 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
  2. class Escalation
  3. attr_reader :ticket
  4. def initialize(ticket, force: false)
  5. @ticket = ticket
  6. @force = force
  7. end
  8. def preferences
  9. @preferences ||= Escalation::TicketPreferences.new(ticket)
  10. end
  11. def biz
  12. @biz ||= calendar&.biz breaks: biz_breaks
  13. end
  14. def biz_breaks
  15. @biz_breaks ||= Escalation::TicketBizBreak.new(ticket, calendar).biz_breaks
  16. end
  17. def escalation_disabled?
  18. @escalation_disabled ||= Ticket::State.lookup(id: ticket.state_id).ignore_escalation?
  19. end
  20. def sla
  21. @sla ||= Sla.for_ticket(ticket)
  22. end
  23. def calendar
  24. @calendar ||= sla&.calendar
  25. end
  26. def forced?
  27. !!@force
  28. end
  29. def force!
  30. @force = true
  31. end
  32. def calculatable?
  33. !escalation_disabled? || preferences.close_at_changed?(ticket) || preferences.last_contact_at_changed?(ticket)
  34. end
  35. def calculate!
  36. calculate
  37. ticket.save! if ticket.has_changes_to_save?
  38. end
  39. def calculate
  40. if !calculatable? && !forced?
  41. calculate_not_calculatable
  42. elsif !calendar
  43. calculate_no_calendar
  44. elsif forced? || any_changes?
  45. enforce_if_needed
  46. update_escalations
  47. update_statistics
  48. apply_preferences
  49. end
  50. end
  51. def any_changes?
  52. preferences.any_changes?(ticket, sla, escalation_disabled?)
  53. end
  54. def assign_reset
  55. ticket.assign_attributes(
  56. escalation_at: nil,
  57. first_response_escalation_at: nil,
  58. update_escalation_at: nil,
  59. close_escalation_at: nil
  60. )
  61. end
  62. def calculate_not_calculatable
  63. assign_reset
  64. apply_preferences if !preferences.hash[:escalation_disabled]
  65. end
  66. def calculate_no_calendar
  67. assign_reset
  68. end
  69. def apply_preferences
  70. preferences.update_preferences(ticket, sla, escalation_disabled?)
  71. end
  72. def enforce_if_needed
  73. return if !preferences.sla_changed?(sla) && !preferences.calendar_changed?(sla.calendar)
  74. force!
  75. end
  76. def update_escalations
  77. ticket.assign_attributes [escalation_first_response, escalation_update, escalation_close]
  78. .compact
  79. .each_with_object({}) { |elem, memo| memo.merge!(elem) }
  80. ticket.escalation_at = calculate_next_escalation
  81. end
  82. def update_statistics
  83. ticket.assign_attributes [statistics_first_response, statistics_update, statistics_close]
  84. .compact
  85. .each_with_object({}) { |elem, memo| memo.merge!(elem) }
  86. end
  87. private
  88. # escalation
  89. # skip escalation neither forced
  90. # nor state switched from closed to open
  91. def skip_escalation?
  92. !forced? && !preferences.escalation_became_enabled?(escalation_disabled?)
  93. end
  94. def escalation_first_response
  95. return if skip_escalation? && !preferences.first_response_at_changed?(ticket)
  96. nullify = escalation_disabled? || ticket.first_response_at.present?
  97. {
  98. first_response_escalation_at: nullify ? nil : calculate_time(ticket.created_at, sla.first_response_time)
  99. }
  100. end
  101. def escalation_update
  102. return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
  103. nullify = escalation_disabled? || ticket.agent_responded?
  104. timestamp = nullify ? nil : ticket.last_contact_customer_at
  105. {
  106. update_escalation_at: timestamp ? calculate_time(timestamp, sla.update_time) : nil
  107. }
  108. end
  109. def escalation_close
  110. return if skip_escalation? && !preferences.close_at_changed?(ticket)
  111. nullify = escalation_disabled? || ticket.close_at.present?
  112. {
  113. close_escalation_at: nullify ? nil : calculate_time(ticket.created_at, sla.solution_time)
  114. }
  115. end
  116. def calculate_time(start_time, span)
  117. return if span.nil? || !span.positive?
  118. Escalation::DestinationTime.new(start_time, span, biz).destination_time
  119. end
  120. def calculate_next_escalation
  121. return if escalation_disabled?
  122. [
  123. (ticket.first_response_escalation_at if !ticket.first_response_at),
  124. ticket.update_escalation_at,
  125. (ticket.close_escalation_at if !ticket.close_at)
  126. ].compact.min
  127. end
  128. # statistics
  129. def skip_statistics_first_response?
  130. return true if !forced? && !preferences.first_response_at_changed?(ticket)
  131. ticket.first_response_at.blank? || sla.first_response_time.blank?
  132. end
  133. def statistics_first_response
  134. return if skip_statistics_first_response?
  135. minutes = calculate_minutes(ticket.created_at, ticket.first_response_at)
  136. {
  137. first_response_in_min: minutes,
  138. first_response_diff_in_min: minutes ? (sla.first_response_time - minutes) : nil
  139. }
  140. end
  141. def skip_statistics_update?
  142. return true if !forced? && !preferences.last_update_at_changed?(ticket)
  143. return true if !sla.update_time
  144. !ticket.agent_responded?
  145. end
  146. # ATTENTION: Recalculation after SLA change won't happen
  147. # SLA change will cause wrong statistics in some edge cases.
  148. # Since this changes `update_in_min` calculation to retain longest timespan.
  149. # But it does not keep track of previous update times.
  150. def statistics_update_applicable?(minutes)
  151. ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
  152. end
  153. def statistics_update
  154. return if skip_statistics_update?
  155. minutes = calculate_minutes(ticket.last_contact_customer_at, ticket.last_contact_agent_at)
  156. return if !forced? && !statistics_update_applicable?(minutes)
  157. {
  158. update_in_min: minutes,
  159. update_diff_in_min: minutes ? (sla.update_time - minutes) : nil
  160. }
  161. end
  162. def skip_statistics_close?
  163. return true if !forced? && !preferences.close_at_changed?(ticket)
  164. ticket.close_at.blank? || sla.solution_time.blank?
  165. end
  166. def statistics_close
  167. return if skip_statistics_close?
  168. minutes = calculate_minutes(ticket.created_at, ticket.close_at)
  169. {
  170. close_in_min: minutes,
  171. close_diff_in_min: minutes ? (sla.solution_time - minutes) : nil
  172. }
  173. end
  174. def calculate_minutes(start_time, end_time)
  175. return if !end_time || !start_time
  176. Escalation::PeriodWorkingMinutes.new(start_time, end_time, ticket, biz).period_working_minutes
  177. end
  178. end