escalation_spec.rb 18 KB

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