ticket.rb 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Ticket < ApplicationModel
  3. include CanBeImported
  4. include HasActivityStreamLog
  5. include ChecksClientNotification
  6. include CanCsvImport
  7. include ChecksHtmlSanitized
  8. include ChecksHumanChanges
  9. include HasHistory
  10. include HasTags
  11. include HasSearchIndexBackend
  12. include HasOnlineNotifications
  13. include HasLinks
  14. include HasObjectManagerAttributes
  15. include HasTaskbars
  16. include Ticket::CallsStatsTicketReopenLog
  17. include Ticket::EnqueuesUserTicketCounterJob
  18. include Ticket::ResetsPendingTimeSeconds
  19. include Ticket::SetsCloseTime
  20. include Ticket::SetsOnlineNotificationSeen
  21. include Ticket::TouchesAssociations
  22. include Ticket::TriggersSubscriptions
  23. include Ticket::ChecksReopenAfterCertainTime
  24. include Ticket::Checklists
  25. include ::Ticket::Escalation
  26. include ::Ticket::Subject
  27. include ::Ticket::Assets
  28. include ::Ticket::SearchIndex
  29. include ::Ticket::Search
  30. include ::Ticket::MergeHistory
  31. include ::Ticket::CanSelector
  32. include ::Ticket::PerformChanges
  33. store :preferences
  34. after_initialize :check_defaults, if: :new_record?
  35. before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
  36. before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
  37. # This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
  38. include Ticket::SetsLastOwnerUpdateTime
  39. # workflow checks should run after before_create and before_update callbacks
  40. # the transaction dispatcher must be run after the workflow checks!
  41. include ChecksCoreWorkflow
  42. include HasTransactionDispatcher
  43. validates :group_id, presence: true
  44. activity_stream_permission 'ticket.agent'
  45. core_workflow_screens 'create_middle', 'edit', 'overview_bulk'
  46. core_workflow_admin_screens 'create_middle', 'edit'
  47. activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
  48. :create_article_type_id,
  49. :create_article_sender_id,
  50. :article_count,
  51. :first_response_at,
  52. :first_response_escalation_at,
  53. :first_response_in_min,
  54. :first_response_diff_in_min,
  55. :close_at,
  56. :close_escalation_at,
  57. :close_in_min,
  58. :close_diff_in_min,
  59. :update_escalation_at,
  60. :update_in_min,
  61. :update_diff_in_min,
  62. :last_close_at,
  63. :last_contact_at,
  64. :last_contact_agent_at,
  65. :last_contact_customer_at,
  66. :last_owner_update_at,
  67. :preferences
  68. search_index_attributes_relevant :organization_id,
  69. :group_id,
  70. :state_id,
  71. :priority_id
  72. history_attributes_ignored :create_article_type_id,
  73. :create_article_sender_id,
  74. :article_count,
  75. :preferences
  76. history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom', 'Checklist', 'Checklist::Item'
  77. validates :note, length: { maximum: 250 }
  78. sanitized_html :note
  79. belongs_to :group, optional: true
  80. belongs_to :organization, optional: true
  81. has_many :articles, -> { reorder(:created_at, :id) }, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
  82. has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
  83. has_many :mentions, as: :mentionable, dependent: :destroy
  84. has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
  85. belongs_to :state, class_name: 'Ticket::State', optional: true
  86. belongs_to :priority, class_name: 'Ticket::Priority', optional: true
  87. belongs_to :owner, class_name: 'User', optional: true
  88. belongs_to :customer, class_name: 'User', optional: true
  89. belongs_to :created_by, class_name: 'User', optional: true
  90. belongs_to :updated_by, class_name: 'User', optional: true
  91. belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
  92. belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
  93. association_attributes_ignored :flags, :mentions
  94. attr_accessor :callback_loop
  95. =begin
  96. processes tickets which have reached their pending time and sets next state_id
  97. processed_tickets = Ticket.process_pending
  98. returns
  99. processed_tickets = [<Ticket>, ...]
  100. =end
  101. def self.process_pending
  102. result = []
  103. # process pending action tickets
  104. pending_action = Ticket::StateType.find_by(name: 'pending action')
  105. ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
  106. .where.not(next_state_id: nil)
  107. if ticket_states_pending_action.present?
  108. next_state_map = {}
  109. ticket_states_pending_action.each do |state|
  110. next_state_map[state.id] = state.next_state_id
  111. end
  112. where(state_id: next_state_map.keys, pending_time: ..Time.current)
  113. .find_each(batch_size: 500) do |ticket|
  114. Transaction.execute do
  115. ticket.state_id = next_state_map[ticket.state_id]
  116. ticket.updated_at = Time.zone.now
  117. ticket.updated_by_id = 1
  118. ticket.save!
  119. end
  120. result.push ticket
  121. end
  122. end
  123. # process pending reminder tickets
  124. pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
  125. ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
  126. if ticket_states_pending_reminder.present?
  127. reminder_state_map = {}
  128. ticket_states_pending_reminder.each do |state|
  129. reminder_state_map[state.id] = state.next_state_id
  130. end
  131. where(state_id: reminder_state_map.keys, pending_time: ..Time.current)
  132. .find_each(batch_size: 500) do |ticket|
  133. article_id = nil
  134. article = Ticket::Article.last_customer_agent_article(ticket.id)
  135. if article
  136. article_id = article.id
  137. end
  138. # send notification
  139. TransactionJob.perform_now(
  140. object: 'Ticket',
  141. type: 'reminder_reached',
  142. object_id: ticket.id,
  143. article_id: article_id,
  144. user_id: 1,
  145. )
  146. result.push ticket
  147. end
  148. end
  149. result
  150. end
  151. def auto_assign(user)
  152. return if !persisted?
  153. return if Setting.get('ticket_auto_assignment').blank?
  154. return if owner_id != 1
  155. return if !TicketPolicy.new(user, self).full?
  156. user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
  157. return if user_ids_ignore.include?(user.id)
  158. ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
  159. return if ticket_auto_assignment_selector.blank?
  160. condition = ticket_auto_assignment_selector[:condition].merge(
  161. 'ticket.id' => {
  162. 'operator' => 'is',
  163. 'value' => id,
  164. }
  165. )
  166. ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full')
  167. return if ticket_count.to_i.zero?
  168. update!(owner: user)
  169. end
  170. =begin
  171. processes escalated tickets
  172. processed_tickets = Ticket.process_escalation
  173. returns
  174. processed_tickets = [<Ticket>, ...]
  175. =end
  176. def self.process_escalation
  177. result = []
  178. # fetch all escalated and soon to be escalating tickets
  179. where(escalation_at: ..15.minutes.from_now)
  180. .find_each(batch_size: 500) do |ticket|
  181. article_id = nil
  182. article = Ticket::Article.last_customer_agent_article(ticket.id)
  183. if article
  184. article_id = article.id
  185. end
  186. # send escalation
  187. if ticket.escalation_at < Time.zone.now
  188. TransactionJob.perform_now(
  189. object: 'Ticket',
  190. type: 'escalation',
  191. object_id: ticket.id,
  192. article_id: article_id,
  193. user_id: 1,
  194. )
  195. result.push ticket
  196. next
  197. end
  198. # check if warning needs to be sent
  199. TransactionJob.perform_now(
  200. object: 'Ticket',
  201. type: 'escalation_warning',
  202. object_id: ticket.id,
  203. article_id: article_id,
  204. user_id: 1,
  205. )
  206. result.push ticket
  207. end
  208. result
  209. end
  210. =begin
  211. processes tickets which auto unassign time has reached
  212. processed_tickets = Ticket.process_auto_unassign
  213. returns
  214. processed_tickets = [<Ticket>, ...]
  215. =end
  216. def self.process_auto_unassign
  217. # process pending action tickets
  218. state_ids = Ticket::State.by_category_ids(:work_on)
  219. return [] if state_ids.blank?
  220. result = []
  221. groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
  222. return [] if groups.blank?
  223. groups.each do |group|
  224. next if group.assignment_timeout.blank?
  225. ticket_ids = Ticket.where('state_id IN (?) AND owner_id != 1 AND group_id = ? AND last_owner_update_at IS NOT NULL', state_ids, group.id).limit(600).pluck(:id)
  226. ticket_ids.each do |ticket_id|
  227. ticket = Ticket.find_by(id: ticket_id)
  228. next if !ticket
  229. minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
  230. next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
  231. Transaction.execute do
  232. ticket.owner_id = 1
  233. ticket.updated_at = Time.zone.now
  234. ticket.updated_by_id = 1
  235. ticket.save!
  236. end
  237. result.push ticket
  238. end
  239. end
  240. result
  241. end
  242. =begin
  243. merge tickets
  244. ticket = Ticket.find(123)
  245. result = ticket.merge_to(
  246. ticket_id: 123,
  247. user_id: 123,
  248. )
  249. returns
  250. result = true|false
  251. =end
  252. def merge_to(data)
  253. # prevent cross merging tickets
  254. target_ticket = Ticket.find_by(id: data[:ticket_id])
  255. raise 'no target ticket given' if !target_ticket
  256. raise Exceptions::UnprocessableEntity, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged'
  257. # check different ticket ids
  258. raise Exceptions::UnprocessableEntity, __('A ticket cannot be merged into itself.') if id == target_ticket.id
  259. # update articles
  260. Transaction.execute context: 'merge' do
  261. Ticket::Article.where(ticket_id: id).each(&:touch)
  262. # quiet update of reassign of articles
  263. Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations
  264. # mark target ticket as updated
  265. # otherwise the "received_merge" history entry
  266. # will be the same as the last updated_at
  267. # which might be a long time ago
  268. target_ticket.updated_at = Time.zone.now
  269. # add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
  270. target_ticket.history_log(
  271. 'received_merge',
  272. data[:user_id],
  273. id_to: target_ticket.id,
  274. id_from: id,
  275. )
  276. history_log(
  277. 'merged_into',
  278. data[:user_id],
  279. id_to: target_ticket.id,
  280. id_from: id,
  281. )
  282. # create new merge article
  283. Ticket::Article.create(
  284. ticket_id: id,
  285. type_id: Ticket::Article::Type.lookup(name: 'note').id,
  286. sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
  287. body: 'merged',
  288. internal: false,
  289. created_by_id: data[:user_id],
  290. updated_by_id: data[:user_id],
  291. )
  292. # search for mention duplicates and destroy them before moving mentions
  293. Mention.duplicates(self, target_ticket).destroy_all
  294. Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
  295. # reassign links to the new ticket
  296. # rubocop:disable Rails/SkipsModelValidations
  297. ticket_source_id = Link::Object.find_by(name: 'Ticket').id
  298. # search for all duplicate source and target links and destroy them
  299. # before link merging
  300. Link.duplicates(
  301. object1_id: ticket_source_id,
  302. object1_value: id,
  303. object2_value: data[:ticket_id]
  304. ).destroy_all
  305. Link.where(
  306. link_object_source_id: ticket_source_id,
  307. link_object_source_value: id,
  308. ).update_all(link_object_source_value: data[:ticket_id])
  309. Link.where(
  310. link_object_target_id: ticket_source_id,
  311. link_object_target_value: id,
  312. ).update_all(link_object_target_value: data[:ticket_id])
  313. # rubocop:enable Rails/SkipsModelValidations
  314. # link tickets
  315. Link.add(
  316. link_type: 'parent',
  317. link_object_source: 'Ticket',
  318. link_object_source_value: data[:ticket_id],
  319. link_object_target: 'Ticket',
  320. link_object_target_value: id
  321. )
  322. # external sync references
  323. ExternalSync.migrate('Ticket', id, target_ticket.id)
  324. # set state to 'merged'
  325. self.state_id = Ticket::State.lookup(name: 'merged').id
  326. # rest owner
  327. self.owner_id = 1
  328. # save ticket
  329. save!
  330. # touch new ticket (to broadcast change)
  331. target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
  332. EventBuffer.add('transaction', {
  333. object: target_ticket.class.name,
  334. type: 'update.received_merge',
  335. data: target_ticket,
  336. changes: {},
  337. id: target_ticket.id,
  338. user_id: UserInfo.current_user_id,
  339. created_at: Time.zone.now,
  340. })
  341. EventBuffer.add('transaction', {
  342. object: self.class.name,
  343. type: 'update.merged_into',
  344. data: self,
  345. changes: {},
  346. id: id,
  347. user_id: UserInfo.current_user_id,
  348. created_at: Time.zone.now,
  349. })
  350. end
  351. true
  352. end
  353. =begin
  354. perform active triggers on ticket
  355. Ticket.perform_triggers(ticket, article, triggers, item, triggers, options)
  356. =end
  357. def self.perform_triggers(ticket, article, triggers, item, options = {})
  358. recursive = Setting.get('ticket_trigger_recursive')
  359. type = options[:type] || item[:type]
  360. local_options = options.clone
  361. local_options[:type] = type
  362. local_options[:reset_user_id] = true
  363. local_options[:disable] = ['Transaction::Notification']
  364. local_options[:trigger_ids] ||= {}
  365. local_options[:trigger_ids][ticket.id.to_s] ||= []
  366. local_options[:loop_count] ||= 0
  367. local_options[:loop_count] += 1
  368. ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
  369. if local_options[:loop_count] > ticket_trigger_recursive_max_loop
  370. message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
  371. logger.info { message }
  372. return [false, message]
  373. end
  374. return [true, __('No triggers active')] if triggers.blank?
  375. # check if notification should be send because of customer emails
  376. send_notification = true
  377. if local_options[:send_notification] == false
  378. send_notification = false
  379. elsif item[:article_id]
  380. article = Ticket::Article.lookup(id: item[:article_id])
  381. if article&.preferences && article.preferences['send-auto-response'] == false
  382. send_notification = false
  383. end
  384. end
  385. Transaction.execute(local_options) do
  386. triggers.each do |trigger|
  387. logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  388. user_id = ticket.updated_by_id
  389. if article
  390. user_id = article.updated_by_id
  391. end
  392. user = User.lookup(id: user_id)
  393. # verify is condition is matching
  394. ticket_count, tickets = Ticket.selectors(
  395. trigger.condition,
  396. limit: 1,
  397. execution_time: true,
  398. current_user: user,
  399. access: 'ignore',
  400. ticket_action: type,
  401. ticket_id: ticket.id,
  402. article_id: article&.id,
  403. changes: item[:changes],
  404. changes_required: trigger.condition_changes_required?
  405. )
  406. next if ticket_count.blank?
  407. next if ticket_count.zero?
  408. next if tickets.take.id != ticket.id
  409. if recursive == false && local_options[:loop_count] > 1
  410. message = "Do not execute recursive triggers per default until Zammad 3.0. With Zammad 3.0 and higher the following trigger is executed '#{trigger.name}' on Ticket:#{ticket.id}. Please review your current triggers and change them if needed."
  411. logger.info { message }
  412. return [true, message]
  413. end
  414. if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
  415. recipient = trigger.perform['notification.email']['recipient']
  416. local_options[:send_notification] = false
  417. if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
  418. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because sender do not want to get auto responder for object (Ticket/#{ticket.id}/Article/#{article.id})" }
  419. next
  420. end
  421. end
  422. if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
  423. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  424. next
  425. end
  426. local_options[:trigger_ids][ticket.id.to_s].push trigger.id
  427. logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  428. ticket.perform_changes(trigger, 'trigger', item, user_id, activator_type: type)
  429. if recursive == true
  430. TransactionDispatcher.commit(local_options)
  431. end
  432. end
  433. end
  434. [true, ticket, local_options]
  435. end
  436. =begin
  437. get all email references headers of a ticket, to exclude some, parse it as array into method
  438. references = ticket.get_references
  439. result
  440. ['message-id-1234', 'message-id-5678']
  441. ignore references header(s)
  442. references = ticket.get_references(['message-id-5678'])
  443. result
  444. ['message-id-1234']
  445. =end
  446. # limited by 32kb (https://github.com/zammad/zammad/issues/5334)
  447. # https://learn.microsoft.com/en-us/office365/servicedescriptions/exchange-online-service-description/exchange-online-limits
  448. def get_references(ignore = [], max_length: 30_000)
  449. references = []
  450. counter = 0
  451. Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).reorder(id: :desc).each do |article|
  452. new_references = []
  453. if article.message_id.present?
  454. new_references.push article.message_id
  455. end
  456. if article.in_reply_to.present?
  457. new_references.push article.in_reply_to
  458. end
  459. new_references -= ignore
  460. counter += new_references.join.length
  461. break if counter > max_length
  462. references.unshift(*new_references)
  463. end
  464. references
  465. end
  466. # Get whichever #last_contact_* was later
  467. # This is not identical to #last_contact_at
  468. # It returns time to last original (versus follow up) contact
  469. # @return [Time, nil]
  470. def last_original_update_at
  471. [last_contact_agent_at, last_contact_customer_at].compact.max
  472. end
  473. # true if conversation did happen and agent responded
  474. # false if customer is waiting for response or agent reached out and customer did not respond yet
  475. # @return [Bool]
  476. def agent_responded?
  477. return false if last_contact_customer_at.blank?
  478. return false if last_contact_agent_at.blank?
  479. last_contact_customer_at < last_contact_agent_at
  480. end
  481. =begin
  482. Get the color of the state the current ticket is in
  483. ticket.current_state_color
  484. returns a hex color code
  485. =end
  486. def current_state_color
  487. return '#f35912' if escalation_at && escalation_at < Time.zone.now
  488. case state.state_type.name
  489. when 'new', 'open'
  490. return '#faab00'
  491. when 'closed'
  492. return '#38ad69'
  493. when 'pending reminder'
  494. return '#faab00' if pending_time && pending_time < Time.zone.now
  495. end
  496. '#000000'
  497. end
  498. def mention_user_ids
  499. mentions.pluck(:user_id)
  500. end
  501. private
  502. def check_generate
  503. return true if number
  504. self.number = Ticket::Number.generate
  505. true
  506. end
  507. def check_title
  508. return true if !title
  509. title.gsub!(%r{\s|\t|\r}, ' ')
  510. true
  511. end
  512. def check_defaults
  513. check_default_owner
  514. check_default_organization
  515. true
  516. end
  517. def check_default_owner
  518. return if !has_attribute?(:owner_id)
  519. return if owner_id || owner
  520. self.owner_id = 1
  521. end
  522. def check_default_organization
  523. return if !has_attribute?(:organization_id)
  524. return if !customer_id
  525. customer = User.find_by(id: customer_id)
  526. return if !customer
  527. return if organization_id.present? && customer.organization_id?(organization_id)
  528. return if organization.present? && customer.organization_id?(organization.id)
  529. self.organization_id = customer.organization_id
  530. end
  531. def reset_pending_time
  532. # ignore if no state has changed
  533. return true if !changes_to_save['state_id']
  534. # ignore if new state is blank and
  535. # let handle ActiveRecord the error
  536. return if state_id.blank?
  537. # check if new state isn't pending*
  538. current_state = Ticket::State.lookup(id: state_id)
  539. current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
  540. # in case, set pending_time to nil
  541. return true if current_state_type.name.match?(%r{^pending}i)
  542. self.pending_time = nil
  543. true
  544. end
  545. def set_default_state
  546. return true if state_id
  547. default_ticket_state = Ticket::State.find_by(default_create: true)
  548. return true if !default_ticket_state
  549. self.state_id = default_ticket_state.id
  550. true
  551. end
  552. def set_default_priority
  553. return true if priority_id
  554. default_ticket_priority = Ticket::Priority.find_by(default_create: true)
  555. return true if !default_ticket_priority
  556. self.priority_id = default_ticket_priority.id
  557. true
  558. end
  559. def check_owner_active
  560. return true if Setting.get('import_mode')
  561. # only change the owner for non closed Tickets for historical/reporting reasons
  562. return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'
  563. # return when ticket is unassigned
  564. return true if owner_id.blank?
  565. return true if owner_id == 1
  566. # return if owner is active, is agent and has access to group of ticket
  567. return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')
  568. # else set the owner of the ticket to the default user as unassigned
  569. self.owner_id = 1
  570. true
  571. end
  572. end