ticket.rb 24 KB

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