escalation.rb 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://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_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_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_reset
  102. return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
  103. return if sla.response_time.present? || sla.update_time.present?
  104. { update_escalation_at: nil }
  105. end
  106. def escalation_response_timestamp
  107. return if escalation_disabled? || ticket.agent_responded?
  108. ticket.last_contact_customer_at
  109. end
  110. def escalation_response
  111. return if sla.response_time.nil?
  112. return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
  113. timestamp = escalation_response_timestamp
  114. {
  115. update_escalation_at: timestamp ? calculate_time(timestamp, sla.response_time) : nil
  116. }
  117. end
  118. def escalation_update_timestamp
  119. return if escalation_disabled?
  120. ticket.last_contact_agent_at || ticket.created_at
  121. end
  122. def escalation_update
  123. return if sla.update_time.nil?
  124. return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
  125. timestamp = escalation_update_timestamp
  126. {
  127. update_escalation_at: timestamp ? calculate_time(timestamp, sla.update_time) : nil
  128. }
  129. end
  130. def escalation_close
  131. return if skip_escalation? && !preferences.close_at_changed?(ticket)
  132. nullify = escalation_disabled? || ticket.close_at.present?
  133. {
  134. close_escalation_at: nullify ? nil : calculate_time(ticket.created_at, sla.solution_time)
  135. }
  136. end
  137. def calculate_time(start_time, span)
  138. return if span.nil? || !span.positive?
  139. Escalation::DestinationTime.new(start_time, span, biz).destination_time
  140. end
  141. def calculate_next_escalation
  142. return if escalation_disabled?
  143. [
  144. (ticket.first_response_escalation_at if !ticket.first_response_at),
  145. ticket.update_escalation_at,
  146. (ticket.close_escalation_at if !ticket.close_at)
  147. ].compact.min
  148. end
  149. # statistics
  150. def skip_statistics_first_response?
  151. return true if !forced? && !preferences.first_response_at_changed?(ticket)
  152. ticket.first_response_at.blank? || sla.first_response_time.blank?
  153. end
  154. def statistics_first_response
  155. return if skip_statistics_first_response?
  156. minutes = calculate_minutes(ticket.created_at, ticket.first_response_at)
  157. {
  158. first_response_in_min: minutes,
  159. first_response_diff_in_min: minutes ? (sla.first_response_time - minutes) : nil
  160. }
  161. end
  162. def skip_statistics_response?
  163. return true if !forced? && !preferences.last_update_at_changed?(ticket)
  164. return true if !sla.response_time
  165. !ticket.agent_responded?
  166. end
  167. # ATTENTION: Recalculation after SLA change won't happen
  168. # SLA change will cause wrong statistics in some edge cases.
  169. # Since this changes `update_in_min` calculation to retain longest timespan.
  170. # But it does not keep track of previous update times.
  171. def statistics_response_applicable?(minutes)
  172. ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
  173. end
  174. def statistics_response
  175. return if skip_statistics_response?
  176. minutes = calculate_minutes(ticket.last_contact_customer_at, ticket.last_contact_agent_at)
  177. return if !forced? && !statistics_response_applicable?(minutes)
  178. {
  179. update_in_min: minutes,
  180. update_diff_in_min: minutes ? (sla.response_time - minutes) : nil
  181. }
  182. end
  183. def skip_statistics_update?
  184. return true if !forced? && !preferences.last_update_at_changed?(ticket)
  185. return true if !sla.update_time
  186. ticket.last_contact_agent_at.blank?
  187. end
  188. # ATTENTION: Recalculation after SLA change won't happen
  189. # SLA change will cause wrong statistics in some edge cases.
  190. # Since this changes `update_in_min` calculation to retain longest timespan.
  191. # But it does not keep track of previous update times.
  192. def statistics_update_applicable?(minutes)
  193. ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
  194. end
  195. def statistics_update_responses
  196. ticket
  197. .articles
  198. .reverse
  199. .lazy
  200. .select { |article| article.sender&.name == 'Agent' && article.type&.communication }
  201. .first(2)
  202. end
  203. def statistics_update_minutes
  204. last_agent_responses = statistics_update_responses
  205. from = last_agent_responses.second&.created_at || ticket.created_at
  206. to = last_agent_responses.first&.created_at
  207. calculate_minutes(from, to)
  208. end
  209. def statistics_update
  210. return if skip_statistics_update?
  211. minutes = statistics_update_minutes
  212. return if !forced? && !statistics_update_applicable?(minutes)
  213. {
  214. update_in_min: minutes,
  215. update_diff_in_min: minutes ? (sla.update_time - minutes) : nil
  216. }
  217. end
  218. def skip_statistics_close?
  219. return true if !forced? && !preferences.close_at_changed?(ticket)
  220. ticket.close_at.blank? || sla.solution_time.blank?
  221. end
  222. def statistics_close
  223. return if skip_statistics_close?
  224. minutes = calculate_minutes(ticket.created_at, ticket.close_at)
  225. {
  226. close_in_min: minutes,
  227. close_diff_in_min: minutes ? (sla.solution_time - minutes) : nil
  228. }
  229. end
  230. def calculate_minutes(start_time, end_time)
  231. return if !end_time || !start_time
  232. Escalation::PeriodWorkingMinutes.new(start_time, end_time, ticket, biz).period_working_minutes
  233. end
  234. end