diff --git a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb index 49806e97c..0ac927058 100644 --- a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb @@ -34,8 +34,11 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account end def set_team - @team = Current.account.teams.find_by(id: params[:team_id]) - @conversation.update!(team: @team) + @team = Conversations::AssignmentService.new( + conversation: @conversation, + team_id: params[:team_id] + ).perform + render json: @team end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index cfde22e70..f45323d12 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -66,6 +66,7 @@ class Conversation < ApplicationRecord validates :inbox_id, presence: true validates :contact_id, presence: true before_validation :validate_additional_attributes + before_validation :reset_agent_bot_when_assignee_present validates :additional_attributes, jsonb_attributes_length: true validates :custom_attributes, jsonb_attributes_length: true validates :uuid, uniqueness: true @@ -116,7 +117,6 @@ class Conversation < ApplicationRecord has_many :attachments, through: :messages has_many :reporting_events, dependent: :destroy_async - before_save :handle_agent_bot_takeover_by_assignee before_save :ensure_snooze_until_reset before_create :determine_conversation_status before_create :ensure_waiting_since @@ -166,9 +166,18 @@ class Conversation < ApplicationRecord def bot_handoff! mark_bot_handoff save! + dispatch_bot_handoff + end + + def dispatch_bot_handoff dispatcher_dispatch(CONVERSATION_BOT_HANDOFF) end + def mark_bot_handoff + self.waiting_since ||= Time.current + self.status = :open + end + def unread_messages agent_last_seen_at.present? ? messages.created_since(agent_last_seen_at) : messages end @@ -271,18 +280,12 @@ class Conversation < ApplicationRecord self.additional_attributes = {} unless additional_attributes.is_a?(Hash) end - def handle_agent_bot_takeover_by_assignee - return if assignee_id.blank? || assignee_agent_bot_id.blank? + def reset_agent_bot_when_assignee_present + return if assignee_id.blank? - mark_bot_handoff if pending? self.assignee_agent_bot_id = nil end - def mark_bot_handoff - self.waiting_since ||= Time.current - self.status = :open - end - def determine_conversation_status self.status = :resolved and return if contact.blocked? @@ -339,7 +342,6 @@ class Conversation < ApplicationRecord CONVERSATION_OPENED => -> { saved_change_to_status? && open? }, CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? }, CONVERSATION_STATUS_CHANGED => -> { saved_change_to_status? }, - CONVERSATION_BOT_HANDOFF => -> { agent_bot_takeover_by_assignee? }, CONVERSATION_READ => -> { saved_change_to_contact_last_seen_at? }, CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? } }.each do |event, condition| @@ -347,13 +349,6 @@ class Conversation < ApplicationRecord end end - def agent_bot_takeover_by_assignee? - assignee_id.present? && - saved_change_to_status?(from: 'pending', to: 'open') && - saved_change_to_assignee_agent_bot_id? && - assignee_agent_bot_id.blank? - end - def dispatcher_dispatch(event_name, changed_attributes = nil) Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?, changed_attributes: changed_attributes, diff --git a/app/services/action_service.rb b/app/services/action_service.rb index 27f513f24..13081424b 100644 --- a/app/services/action_service.rb +++ b/app/services/action_service.rb @@ -41,7 +41,7 @@ class ActionService end def assign_agent(agent_ids = []) - return @conversation.update!(assignee_id: nil) if agent_ids[0] == 'nil' + return assign_conversation_agent(nil) if agent_ids[0] == 'nil' agent_ids = [last_responding_agent_id] if agent_ids[0] == 'last_responding_agent' return unless agent_belongs_to_inbox?(agent_ids) @@ -49,7 +49,7 @@ class ActionService @agent = @account.users.find_by(id: agent_ids) return unless @agent.present? && @agent.confirmed? - @conversation.update!(assignee_id: @agent.id) + assign_conversation_agent(@agent.id) end def remove_label(labels) @@ -62,21 +62,21 @@ class ActionService def assign_team(team_ids = []) # Keep nil/0 handling for existing automation and macro payloads. should_unassign = team_ids.blank? || %w[nil 0].include?(team_ids[0].to_s) - return @conversation.update!(team_id: nil) if should_unassign + return assign_conversation_team(nil) if should_unassign # check if team belongs to account only if team_id is present # if team_id is nil, then it means that the team is being unassigned return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids) - @conversation.update!(team_id: team_ids[0]) + assign_conversation_team(team_ids[0]) end def remove_assigned_agent(_params) - @conversation.update!(assignee_id: nil) + assign_conversation_agent(nil) end def remove_assigned_team(_params) - @conversation.update!(team_id: nil) + assign_conversation_team(nil) end def send_email_transcript(emails) @@ -99,6 +99,14 @@ class ActionService @conversation.messages.outgoing.where(sender_type: 'User', private: false).last&.sender_id end + def assign_conversation_agent(assignee_id) + Conversations::AssignmentService.new(conversation: @conversation, assignee_id: assignee_id).perform + end + + def assign_conversation_team(team_id) + Conversations::AssignmentService.new(conversation: @conversation, team_id: team_id).perform + end + def agent_belongs_to_inbox?(agent_ids) member_ids = @conversation.inbox.members.pluck(:user_id) assignable_agent_ids = member_ids + @account.administrators.ids diff --git a/app/services/conversations/assignment_service.rb b/app/services/conversations/assignment_service.rb index c73d41342..743b54be3 100644 --- a/app/services/conversations/assignment_service.rb +++ b/app/services/conversations/assignment_service.rb @@ -1,22 +1,33 @@ class Conversations::AssignmentService - def initialize(conversation:, assignee_id:, assignee_type: nil) + TEAM_ID_UNSET = Object.new.freeze + private_constant :TEAM_ID_UNSET + + def initialize(conversation:, assignee_id: nil, assignee_type: nil, team_id: TEAM_ID_UNSET) @conversation = conversation @assignee_id = assignee_id @assignee_type = assignee_type + @team_id = team_id end def perform + return assign_team if team_assignment? + agent_bot_assignment? ? assign_agent_bot : assign_agent end private - attr_reader :conversation, :assignee_id, :assignee_type + attr_reader :conversation, :assignee_id, :assignee_type, :team_id def assign_agent + bot_handoff = bot_handoff_to_human?(assignee) + conversation.assignee = assignee - conversation.assignee_agent_bot = nil if assignee.blank? + conversation.assignee_agent_bot = nil + conversation.mark_bot_handoff if bot_handoff conversation.save! + conversation.dispatch_bot_handoff if bot_handoff + assignee end @@ -30,6 +41,20 @@ class Conversations::AssignmentService agent_bot end + def assign_team + conversation.team = team + validate_current_assignee_team + conversation.assignee ||= find_assignee_from_team + + bot_handoff = bot_handoff_to_human?(conversation.assignee) + conversation.assignee_agent_bot = nil if conversation.assignee.present? + conversation.mark_bot_handoff if bot_handoff + conversation.save! + conversation.dispatch_bot_handoff if bot_handoff + + team + end + def assignee @assignee ||= conversation.account.users.find_by(id: assignee_id) end @@ -41,4 +66,27 @@ class Conversations::AssignmentService def agent_bot_assignment? assignee_type.to_s == 'AgentBot' end + + def team_assignment? + team_id != TEAM_ID_UNSET + end + + def team + @team ||= conversation.account.teams.find_by(id: team_id) + end + + def validate_current_assignee_team + conversation.assignee = nil if team&.members&.exclude?(conversation.assignee) + end + + def find_assignee_from_team + return if team&.allow_auto_assign.blank? + + team_members_with_capacity = conversation.inbox.member_ids_with_assignment_capacity & team.members.ids + ::AutoAssignment::AgentAssignmentService.new(conversation: conversation, allowed_agent_ids: team_members_with_capacity).find_assignee + end + + def bot_handoff_to_human?(assignee) + assignee.present? && conversation.assignee_agent_bot.present? && conversation.pending? + end end