refactor: route assignment handoff through service

This commit is contained in:
Sojan Jose
2026-07-06 21:56:12 -07:00
parent 1f35360ecb
commit ce178cd3fe
4 changed files with 82 additions and 28 deletions
@@ -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
+12 -17
View File
@@ -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,
+14 -6
View File
@@ -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
@@ -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