escalation_spec.rb 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe ::Escalation do
  4. let(:instance) { described_class.new ticket, force: force }
  5. let(:instance_with_history) { described_class.new ticket_with_history, force: force }
  6. let(:ticket) { create(:ticket) }
  7. let(:force) { false }
  8. let(:sla) { nil }
  9. let(:sla_247) { create(:sla, :condition_blank, first_response_time: 60, update_time: 60, solution_time: 75, calendar: create(:calendar, :'24/7')) }
  10. let(:calendar) { nil }
  11. let(:ticket_with_history) do
  12. freeze_time
  13. ticket = create(:ticket)
  14. ticket.update! state: Ticket::State.lookup(name: 'new')
  15. travel 1.hour
  16. ticket.update! state: Ticket::State.lookup(name: 'open')
  17. travel 30.minutes
  18. ticket.update! state: Ticket::State.lookup(name: 'pending close')
  19. travel 30.minutes
  20. ticket.update! state: Ticket::State.lookup(name: 'closed'), close_at: Time.current
  21. ticket
  22. end
  23. let(:open_ticket_with_history) do
  24. freeze_time
  25. article = create(:ticket_article, :inbound_email)
  26. travel 10.minutes
  27. article.ticket.update! state: Ticket::State.lookup(name: 'pending close')
  28. travel 10.minutes
  29. article.ticket.update! state: Ticket::State.lookup(name: 'open')
  30. article.ticket
  31. end
  32. describe '#preferences' do
  33. it { expect(instance.preferences).to be_a Escalation::TicketPreferences }
  34. end
  35. describe '#escalation_disabled?' do
  36. it 'true when ticket is not open' do
  37. ticket.update! state: Ticket::State.lookup(name: 'pending close')
  38. expect(instance).to be_escalation_disabled
  39. end
  40. it 'false when ticket is open' do
  41. expect(instance).not_to be_escalation_disabled
  42. end
  43. end
  44. describe '#calculatable?' do
  45. it 'false when ticket is not open' do
  46. ticket.update! state: Ticket::State.lookup(name: 'pending close')
  47. expect(instance).not_to be_calculatable
  48. end
  49. it 'true when ticket is open' do
  50. expect(instance).to be_calculatable
  51. end
  52. # https://github.com/zammad/zammad/issues/2579
  53. it 'true when ticket was just closed' do
  54. ticket
  55. travel 30.minutes
  56. without_update_escalation_information_callback { ticket.update close_at: Time.current, state: Ticket::State.lookup(name: 'closed') }
  57. expect(instance).to be_calculatable
  58. end
  59. it 'true when response to ticket comes while ticket has pending reminder' do
  60. ticket.update(state: Ticket::State.find_by(name: 'pending reminder'))
  61. without_update_escalation_information_callback { create(:'ticket/article', :outbound_email, ticket: ticket) }
  62. expect(instance).to be_calculatable
  63. end
  64. end
  65. describe '#calculate' do
  66. it 'works and updates' do
  67. ticket
  68. sla_247
  69. expect { instance.calculate }.to change(ticket, :has_changes_to_save?).to(true)
  70. end
  71. it 'exit early when escalation is disabled' do
  72. allow(instance).to receive(:escalation_disabled?).and_return(true)
  73. allow(instance).to receive(:calendar) # next method called after checking escalation state
  74. instance.calculate
  75. expect(instance).not_to have_received(:calendar)
  76. end
  77. it 'recalculate when escalation is disabled but it is forced' do
  78. instance_forced = described_class.new ticket, force: true
  79. allow(instance_forced).to receive(:escalation_disabled?).and_return(true)
  80. allow(instance_forced).to receive(:calendar) # next method called after checking escalation state
  81. instance_forced.calculate
  82. expect(instance_forced).to have_received(:calendar)
  83. end
  84. it 'no calendar is early exit' do
  85. allow(instance).to receive(:calendar).and_return(nil)
  86. allow(instance.preferences).to receive(:any_changes?) # next method after the check
  87. instance.calculate
  88. expect(instance.preferences).not_to have_received(:any_changes?)
  89. end
  90. it 'no calendar resets' do
  91. allow(instance).to receive(:calendar).and_return(nil)
  92. allow(instance).to receive(:forced?).and_return(true)
  93. allow(instance).to receive(:calculate_no_calendar)
  94. instance.calculate
  95. expect(instance).to have_received(:calculate_no_calendar)
  96. end
  97. context 'with SLA 24/7' do
  98. before { sla_247 }
  99. it 'forces recalculation when SLA touched' do
  100. allow(instance.preferences).to receive(:sla_changed?).and_return(true)
  101. allow(instance).to receive(:force!)
  102. instance.calculate
  103. expect(instance).to have_received(:force!)
  104. end
  105. it 'calculates when ticket was touched in a related manner' do
  106. allow(instance.preferences).to receive(:any_changes?).and_return(true)
  107. allow(instance).to receive(:update_escalations)
  108. instance.calculate
  109. expect(instance).to have_received(:update_escalations)
  110. end
  111. it 'skips calculating escalation times when ticket was not touched in a related manner' do
  112. allow(instance.preferences).to receive(:any_changes?).and_return(false)
  113. allow(instance).to receive(:update_escalations)
  114. instance.calculate
  115. expect(instance).not_to have_received(:update_escalations)
  116. end
  117. it 'calculates statistics when ticket was touched in a related manner' do
  118. allow(instance.preferences).to receive(:any_changes?).and_return(true)
  119. allow(instance).to receive(:update_statistics)
  120. instance.calculate
  121. expect(instance).to have_received(:update_statistics)
  122. end
  123. it 'skips calculating statistics when ticket was not touched in a related manner' do
  124. allow(instance.preferences).to receive(:any_changes?).and_return(false)
  125. allow(instance).to receive(:update_statistics)
  126. instance.calculate
  127. expect(instance).not_to have_received(:update_statistics)
  128. end
  129. it 'setting #first_response_at does not nullify other escalations' do
  130. ticket.update! first_response_at: 30.minutes.from_now
  131. expect(ticket.reload.close_escalation_at).not_to be_nil
  132. end
  133. it 'setting ticket to non-escalatable state clears #escalation_at' do
  134. ticket.update! state: Ticket::State.lookup(name: 'closed')
  135. expect(ticket.escalation_at).to be_nil
  136. end
  137. # https://github.com/zammad/zammad/issues/2579
  138. it 'calculates closing statistics on closing ticket' do
  139. ticket
  140. travel 30.minutes
  141. without_update_escalation_information_callback { ticket.update close_at: Time.current, state: Ticket::State.lookup(name: 'closed') }
  142. expect { instance.calculate }.to change(ticket, :close_in_min).from(nil)
  143. end
  144. end
  145. end
  146. describe '#force!' do
  147. it 'sets forced? to true' do
  148. expect { instance.send(:force!) }.to change(instance, :forced?).from(false).to(true)
  149. end
  150. end
  151. describe 'calculate_not_calculatable' do
  152. it 'sets escalation dates to nil' do
  153. sla_247
  154. open_ticket_with_history
  155. instance = described_class.new open_ticket_with_history
  156. instance.calculate_not_calculatable
  157. expect(open_ticket_with_history).to have_attributes(escalation_at: nil, first_response_escalation_at: nil, update_escalation_at: nil, close_escalation_at: nil)
  158. end
  159. end
  160. describe '#sla' do
  161. it 'returns SLA when it exists' do
  162. sla_247
  163. expect(instance.sla).to be_a(Sla)
  164. end
  165. it 'returns nil when no SLA' do
  166. expect(instance.sla).to be_nil
  167. end
  168. end
  169. describe '#calendar' do
  170. it 'returns calendar when it exists' do
  171. sla_247
  172. expect(instance.calendar).to be_a(Calendar)
  173. end
  174. it 'returns nil when no calendar' do
  175. expect(instance.calendar).to be_nil
  176. end
  177. end
  178. describe '#forced?' do
  179. it 'true when given true' do
  180. instance = described_class.new ticket, force: true
  181. expect(instance).to be_forced
  182. end
  183. it 'false when given false' do
  184. instance = described_class.new ticket, force: false
  185. expect(instance).not_to be_forced
  186. end
  187. it 'false when given nil' do
  188. instance = described_class.new ticket, force: nil
  189. expect(instance).not_to be_forced
  190. end
  191. end
  192. describe '#update_escalations' do
  193. it 'sets escalation times' do
  194. instance = described_class.new open_ticket_with_history
  195. sla_247
  196. expect { instance.update_escalations }
  197. .to change(open_ticket_with_history, :escalation_at).from(nil)
  198. end
  199. # https://github.com/zammad/zammad/issues/3140
  200. it 'agent follow up does not set #update_escalation_at' do
  201. sla_247
  202. ticket
  203. create(:ticket_article, :outbound_email, ticket: ticket)
  204. expect(ticket.reload.update_escalation_at).to be_nil
  205. end
  206. # https://github.com/zammad/zammad/issues/3140
  207. it 'customer contact sets #update_escalation_at' do
  208. sla_247
  209. ticket
  210. create(:ticket_article, :inbound_email, ticket: ticket)
  211. expect(ticket.reload.update_escalation_at).to be_a(Time)
  212. end
  213. context 'with ticket with sla and customer enquiry' do
  214. before do
  215. sla_247
  216. ticket
  217. travel 10.minutes
  218. create(:ticket_article, :inbound_email, ticket: ticket)
  219. travel 10.minutes
  220. end
  221. # https://github.com/zammad/zammad/issues/3140
  222. it 'agent response clears #update_escalation_at' do
  223. expect { create(:ticket_article, :outbound_email, ticket: ticket) }
  224. .to change { ticket.reload.update_escalation_at }.to(nil)
  225. end
  226. # https://github.com/zammad/zammad/issues/3140
  227. it 'repeated customer requests do not #update_escalation_at' do
  228. expect { create(:ticket_article, :inbound_email, ticket: ticket) }
  229. .not_to change { ticket.reload.update_escalation_at }
  230. end
  231. end
  232. end
  233. describe '#escalation_first_response' do
  234. let(:force) { true } # initial calculation
  235. it 'returns attribute' do
  236. sla_247
  237. allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
  238. result = instance_with_history.send(:escalation_first_response)
  239. expect(result).to include first_response_escalation_at: 60.minutes.ago
  240. end
  241. it 'returns nil when no sla#first_response_time' do
  242. sla_247.update! first_response_time: nil
  243. allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
  244. result = instance_with_history.send(:escalation_first_response)
  245. expect(result).to include(first_response_escalation_at: nil)
  246. end
  247. end
  248. describe '#escalation_update' do
  249. it 'returns attribute' do
  250. sla_247
  251. ticket_with_history.last_contact_customer_at = 2.hours.ago
  252. allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
  253. result = instance_with_history.send(:escalation_update)
  254. expect(result).to include update_escalation_at: 60.minutes.ago
  255. end
  256. it 'returns nil when no sla#update_time' do
  257. sla_247.update! update_time: nil
  258. allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
  259. result = instance_with_history.send(:escalation_update)
  260. expect(result).to include(update_escalation_at: nil)
  261. end
  262. end
  263. describe '#escalation_close' do
  264. it 'returns attribute' do
  265. sla_247
  266. ticket_with_history.update! state: Ticket::State.lookup(name: 'open'), close_at: nil
  267. allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
  268. result = instance_with_history.send(:escalation_close)
  269. expect(result).to include close_escalation_at: 45.minutes.ago
  270. end
  271. it 'returns nil when no sla#solution_time' do
  272. sla_247.update! solution_time: nil
  273. allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
  274. result = instance_with_history.send(:escalation_close)
  275. expect(result).to include(close_escalation_at: nil)
  276. end
  277. end
  278. describe '#calculate_time' do
  279. before do
  280. sla_247
  281. start
  282. end
  283. let(:start) { 75.minutes.from_now.change(sec: 0) }
  284. it 'calculates target time that is given working minutes after start time' do
  285. expect(instance_with_history.send(:calculate_time, start, 30)).to eq(start + 1.hour)
  286. end
  287. it 'returns nil when given 0 span' do
  288. expect(instance_with_history.send(:calculate_time, start, 0)).to be_nil
  289. end
  290. it 'returns nil when given no span' do
  291. expect(instance_with_history.send(:calculate_time, start, nil)).to be_nil
  292. end
  293. end
  294. describe '#calculate_next_escalation' do
  295. it 'nil when escalation is disabled' do
  296. ticket.update! state: Ticket::State.lookup(name: 'closed')
  297. expect(instance.send(:calculate_next_escalation)).to be_nil
  298. end
  299. it 'first_response_escalation_at when earliest' do
  300. ticket.update! first_response_escalation_at: 1.hour.from_now, update_escalation_at: 2.hours.from_now, close_escalation_at: 3.hours.from_now
  301. expect(instance.send(:calculate_next_escalation)).to eq ticket.first_response_escalation_at
  302. end
  303. it 'update_escalation_at when earliest' do
  304. ticket.update! first_response_escalation_at: 2.hours.from_now, update_escalation_at: 1.hour.from_now, close_escalation_at: 3.hours.from_now
  305. expect(instance.send(:calculate_next_escalation)).to eq ticket.update_escalation_at
  306. end
  307. it 'close_escalation_at when earliest' do
  308. ticket.update! first_response_escalation_at: 2.hours.from_now, update_escalation_at: 1.hour.from_now, close_escalation_at: 30.minutes.from_now
  309. expect(instance.send(:calculate_next_escalation)).to eq ticket.close_escalation_at
  310. end
  311. it 'works when one of escalation times is not present' do
  312. ticket.update! first_response_escalation_at: 1.hour.from_now, update_escalation_at: nil, close_escalation_at: nil
  313. expect { instance.send(:calculate_next_escalation) }.not_to raise_error
  314. end
  315. end
  316. describe '#statistics_first_response' do
  317. it 'calculates statistics' do
  318. sla_247
  319. ticket_with_history.first_response_at = 45.minutes.ago
  320. instance_with_history.force!
  321. result = instance_with_history.send(:statistics_first_response)
  322. expect(result).to include(first_response_in_min: 75, first_response_diff_in_min: -15)
  323. end
  324. it 'does not touch statistics when sla time is nil' do
  325. sla_247.update! first_response_time: nil
  326. ticket_with_history.first_response_at = 45.minutes.ago
  327. instance_with_history.force!
  328. result = instance_with_history.send(:statistics_first_response)
  329. expect(result).to be_nil
  330. end
  331. end
  332. describe '#statistics_update' do
  333. before do
  334. sla_247
  335. freeze_time
  336. end
  337. it 'calculates statistics' do
  338. ticket_with_history.last_contact_customer_at = 61.minutes.ago
  339. ticket_with_history.last_contact_agent_at = 60.minutes.ago
  340. result = instance_with_history.send(:statistics_update)
  341. expect(result).to include(update_in_min: 1, update_diff_in_min: 59)
  342. end
  343. it 'does not calculate statistics when customer respose is last' do
  344. ticket_with_history.last_contact_customer_at = 59.minutes.ago
  345. ticket_with_history.last_contact_agent_at = 60.minutes.ago
  346. result = instance_with_history.send(:statistics_update)
  347. expect(result).to be_nil
  348. end
  349. it 'does not calculate statistics when only customer enquiry present' do
  350. create(:ticket_article, :inbound_email, ticket: ticket)
  351. result = instance.send(:statistics_update)
  352. expect(result).to be_nil
  353. end
  354. it 'calculates update statistics of last exchange' do
  355. create(:ticket_article, :inbound_email, ticket: ticket)
  356. travel 10.minutes
  357. create(:ticket_article, :outbound_email, ticket: ticket)
  358. instance.force!
  359. expect(instance.send(:statistics_update)).to include(update_in_min: 10, update_diff_in_min: 50)
  360. end
  361. context 'with multiple exchanges and later one being quicker' do
  362. before do
  363. create(:ticket_article, :inbound_email, ticket: ticket)
  364. travel 10.minutes
  365. create(:ticket_article, :outbound_email, ticket: ticket)
  366. travel 10.minutes
  367. create(:ticket_article, :inbound_email, ticket: ticket)
  368. travel 5.minutes
  369. create(:ticket_article, :outbound_email, ticket: ticket)
  370. end
  371. it 'keeps statistics of longest exchange' do
  372. expect(ticket.reload).to have_attributes(update_in_min: 10, update_diff_in_min: 50)
  373. end
  374. end
  375. it 'does not touch statistics when sla time is nil' do
  376. sla_247.update! update_time: nil
  377. ticket_with_history.last_contact_customer_at = 60.minutes.ago
  378. instance_with_history.force!
  379. result = instance_with_history.send(:statistics_update)
  380. expect(result).to be_nil
  381. end
  382. it 'does not touch statistics when last update is nil' do
  383. ticket_with_history.assign_attributes last_contact_agent_at: nil, last_contact_customer_at: nil
  384. instance_with_history.force!
  385. result = instance_with_history.send(:statistics_update)
  386. expect(result).to be_nil
  387. end
  388. end
  389. describe '#statistics_close' do
  390. it 'calculates statistics' do
  391. sla_247
  392. ticket_with_history.close_at = 50.minutes.ago
  393. instance_with_history.force!
  394. result = instance_with_history.send(:statistics_close)
  395. expect(result).to include(close_in_min: 70, close_diff_in_min: 5)
  396. end
  397. it 'does not touch statistics when sla time is nil' do
  398. sla_247.update! solution_time: nil
  399. ticket_with_history.close_at = 50.minutes.ago
  400. instance_with_history.force!
  401. result = instance_with_history.send(:statistics_close)
  402. expect(result).to be_nil
  403. end
  404. end
  405. describe '#calculate_minutes' do
  406. it 'calculates working minutes up to given time' do
  407. sla_247
  408. expect(instance_with_history.send(:calculate_minutes, ticket_with_history.created_at, 90.minutes.ago)).to be 30
  409. end
  410. it 'returns nil when given nil' do
  411. sla_247
  412. expect(instance.send(:calculate_minutes, ticket.created_at, nil)).to be_nil
  413. end
  414. end
  415. it 'switching state pushes escalation date' do
  416. sla_247
  417. open_ticket_with_history.reload
  418. expect(open_ticket_with_history.update_escalation_at).to eq open_ticket_with_history.created_at + 70.minutes
  419. end
  420. def without_update_escalation_information_callback(&block)
  421. Ticket.without_callback(:commit, :after, :update_escalation_information, &block)
  422. end
  423. end