feat: APIs to assign agents_bots as assignee in conversations (#12836)

## Summary
- add an assignee_agent_bot_id column as an initital step to prototype
this before fully switching to polymorphic assignee
- update assignment APIs and conversation list / show endpoints to
reflect assignee as agent bot
- ensure webhook payloads contains agent bot assignee


[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912833377e48326b6641b9eee32d50f)

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose
2025-11-18 18:20:58 -08:00
committed by GitHub
parent 70c183ea6e
commit 5f2b2f4221
23 changed files with 316 additions and 52 deletions

View File

@@ -4,7 +4,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
before_action :agent_bot, except: [:index, :create] before_action :agent_bot, except: [:index, :create]
def index def index
@agent_bots = AgentBot.where(account_id: [nil, Current.account.id]) @agent_bots = AgentBot.accessible_to(Current.account)
end end
def show; end def show; end
@@ -37,7 +37,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
private private
def agent_bot def agent_bot
@agent_bot = AgentBot.where(account_id: [nil, Current.account.id]).find(params[:id]) if params[:action] == 'show' @agent_bot = AgentBot.accessible_to(Current.account).find(params[:id]) if params[:action] == 'show'
@agent_bot ||= Current.account.agent_bots.find(params[:id]) @agent_bot ||= Current.account.agent_bots.find(params[:id])
end end

View File

@@ -1,7 +1,7 @@
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
# assigns agent/team to a conversation # assigns agent/team to a conversation
def create def create
if params.key?(:assignee_id) if params.key?(:assignee_id) || agent_bot_assignment?
set_agent set_agent
elsif params.key?(:team_id) elsif params.key?(:team_id)
set_team set_team
@@ -13,17 +13,23 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
private private
def set_agent def set_agent
@agent = Current.account.users.find_by(id: params[:assignee_id]) resource = Conversations::AssignmentService.new(
@conversation.assignee = @agent conversation: @conversation,
@conversation.save! assignee_id: params[:assignee_id],
render_agent assignee_type: params[:assignee_type]
).perform
render_agent(resource)
end end
def render_agent def render_agent(resource)
if @agent.nil? case resource
render json: nil when User
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: resource }
when AgentBot
render partial: 'api/v1/models/agent_bot_slim', formats: [:json], locals: { resource: resource }
else else
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent } render json: nil
end end
end end
@@ -32,4 +38,8 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
@conversation.update!(team: @team) @conversation.update!(team: @team)
render json: @team render json: @team
end end
def agent_bot_assignment?
params[:assignee_type].to_s == 'AgentBot'
end
end end

View File

@@ -25,6 +25,9 @@ module EnsureCurrentAccountHelper
end end
def account_accessible_for_bot?(account) def account_accessible_for_bot?(account)
render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) return if @resource.account_id == account.id
return if @resource.agent_bot_inboxes.find_by(account_id: account.id)
render_unauthorized('Bot is not authorized to access this account')
end end
end end

View File

@@ -63,10 +63,9 @@ class ConversationApi extends ApiClient {
} }
assignAgent({ conversationId, agentId }) { assignAgent({ conversationId, agentId }) {
return axios.post( return axios.post(`${this.url}/${conversationId}/assignments`, {
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, assignee_id: agentId,
{} });
);
} }
assignTeam({ conversationId, teamId }) { assignTeam({ conversationId, teamId }) {

View File

@@ -92,8 +92,10 @@ describe('#ConversationAPI', () => {
it('#assignAgent', () => { it('#assignAgent', () => {
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 }); conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
expect(axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments?assignee_id=34`, `/api/v1/conversations/12/assignments`,
{} {
assignee_id: 34,
}
); );
}); });

View File

@@ -30,7 +30,7 @@ const assignedAgent = computed({
return currentChat.value?.meta?.assignee; return currentChat.value?.meta?.assignee;
}, },
set(agent) { set(agent) {
const agentId = agent ? agent.id : 0; const agentId = agent ? agent.id : null;
store.dispatch('setCurrentChatAssignee', agent); store.dispatch('setCurrentChatAssignee', agent);
store.dispatch('assignAgent', { store.dispatch('assignAgent', {
conversationId: currentChat.value?.id, conversationId: currentChat.value?.id,

View File

@@ -84,7 +84,7 @@ export default {
return this.currentChat.meta.assignee; return this.currentChat.meta.assignee;
}, },
set(agent) { set(agent) {
const agentId = agent ? agent.id : 0; const agentId = agent ? agent.id : null;
this.$store.dispatch('setCurrentChatAssignee', agent); this.$store.dispatch('setCurrentChatAssignee', agent);
this.$store this.$store
.dispatch('assignAgent', { .dispatch('assignAgent', {

View File

@@ -2,61 +2,60 @@ class AgentBotListener < BaseListener
def conversation_resolved(event) def conversation_resolved(event)
conversation = extract_conversation_and_account(event)[0] conversation = extract_conversation_and_account(event)[0]
inbox = conversation.inbox inbox = conversation.inbox
return unless connected_agent_bot_exist?(inbox)
event_name = __method__.to_s event_name = __method__.to_s
payload = conversation.webhook_data.merge(event: event_name) payload = conversation.webhook_data.merge(event: event_name)
process_webhook_bot_event(inbox.agent_bot, payload) agent_bots_for(inbox, conversation).each { |agent_bot| process_webhook_bot_event(agent_bot, payload) }
end end
def conversation_opened(event) def conversation_opened(event)
conversation = extract_conversation_and_account(event)[0] conversation = extract_conversation_and_account(event)[0]
inbox = conversation.inbox inbox = conversation.inbox
return unless connected_agent_bot_exist?(inbox)
event_name = __method__.to_s event_name = __method__.to_s
payload = conversation.webhook_data.merge(event: event_name) payload = conversation.webhook_data.merge(event: event_name)
process_webhook_bot_event(inbox.agent_bot, payload) agent_bots_for(inbox, conversation).each { |agent_bot| process_webhook_bot_event(agent_bot, payload) }
end end
def message_created(event) def message_created(event)
message = extract_message_and_account(event)[0] message = extract_message_and_account(event)[0]
inbox = message.inbox inbox = message.inbox
return unless connected_agent_bot_exist?(inbox)
return unless message.webhook_sendable? return unless message.webhook_sendable?
method_name = __method__.to_s method_name = __method__.to_s
process_message_event(method_name, inbox.agent_bot, message, event) agent_bots_for(inbox, message.conversation).each { |agent_bot| process_message_event(method_name, agent_bot, message, event) }
end end
def message_updated(event) def message_updated(event)
message = extract_message_and_account(event)[0] message = extract_message_and_account(event)[0]
inbox = message.inbox inbox = message.inbox
return unless connected_agent_bot_exist?(inbox)
return unless message.webhook_sendable? return unless message.webhook_sendable?
method_name = __method__.to_s method_name = __method__.to_s
process_message_event(method_name, inbox.agent_bot, message, event) agent_bots_for(inbox, message.conversation).each { |agent_bot| process_message_event(method_name, agent_bot, message, event) }
end end
def webwidget_triggered(event) def webwidget_triggered(event)
contact_inbox = event.data[:contact_inbox] contact_inbox = event.data[:contact_inbox]
inbox = contact_inbox.inbox inbox = contact_inbox.inbox
return unless connected_agent_bot_exist?(inbox)
event_name = __method__.to_s event_name = __method__.to_s
payload = contact_inbox.webhook_data.merge(event: event_name) payload = contact_inbox.webhook_data.merge(event: event_name)
payload[:event_info] = event.data[:event_info] payload[:event_info] = event.data[:event_info]
process_webhook_bot_event(inbox.agent_bot, payload) agent_bots_for(inbox).each { |agent_bot| process_webhook_bot_event(agent_bot, payload) }
end end
private private
def connected_agent_bot_exist?(inbox) def agent_bots_for(inbox, conversation = nil)
return if inbox.agent_bot_inbox.blank? bots = []
return unless inbox.agent_bot_inbox.active? bots << conversation.assignee_agent_bot if conversation&.assignee_agent_bot.present?
inbox_bot = active_inbox_agent_bot(inbox)
bots << inbox_bot if inbox_bot.present?
bots.compact.uniq
end
true def active_inbox_agent_bot(inbox)
return unless inbox.agent_bot_inbox&.active?
inbox.agent_bot
end end
def process_message_event(method_name, agent_bot, message, _event) def process_message_event(method_name, agent_bot, message, _event)

View File

@@ -21,9 +21,18 @@ class AgentBot < ApplicationRecord
include AccessTokenable include AccessTokenable
include Avatarable include Avatarable
scope :accessible_to, lambda { |account|
account_id = account&.id
where(account_id: [nil, account_id])
}
has_many :agent_bot_inboxes, dependent: :destroy_async has_many :agent_bot_inboxes, dependent: :destroy_async
has_many :inboxes, through: :agent_bot_inboxes has_many :inboxes, through: :agent_bot_inboxes
has_many :messages, as: :sender, dependent: :nullify has_many :messages, as: :sender, dependent: :nullify
has_many :assigned_conversations, class_name: 'Conversation',
foreign_key: :assignee_agent_bot_id,
dependent: :nullify,
inverse_of: :assignee_agent_bot
belongs_to :account, optional: true belongs_to :account, optional: true
enum bot_type: { webhook: 0 } enum bot_type: { webhook: 0 }

View File

@@ -20,6 +20,7 @@
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :integer not null # account_id :integer not null
# assignee_agent_bot_id :bigint
# assignee_id :integer # assignee_id :integer
# campaign_id :bigint # campaign_id :bigint
# contact_id :bigint # contact_id :bigint
@@ -65,6 +66,7 @@ class Conversation < ApplicationRecord
validates :inbox_id, presence: true validates :inbox_id, presence: true
validates :contact_id, presence: true validates :contact_id, presence: true
before_validation :validate_additional_attributes before_validation :validate_additional_attributes
before_validation :reset_agent_bot_when_assignee_present
validates :additional_attributes, jsonb_attributes_length: true validates :additional_attributes, jsonb_attributes_length: true
validates :custom_attributes, jsonb_attributes_length: true validates :custom_attributes, jsonb_attributes_length: true
validates :uuid, uniqueness: true validates :uuid, uniqueness: true
@@ -98,6 +100,7 @@ class Conversation < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :inbox belongs_to :inbox
belongs_to :assignee, class_name: 'User', optional: true, inverse_of: :assigned_conversations belongs_to :assignee, class_name: 'User', optional: true, inverse_of: :assigned_conversations
belongs_to :assignee_agent_bot, class_name: 'AgentBot', optional: true
belongs_to :contact belongs_to :contact
belongs_to :contact_inbox belongs_to :contact_inbox
belongs_to :team, optional: true belongs_to :team, optional: true
@@ -180,6 +183,18 @@ class Conversation < ApplicationRecord
true true
end end
# Virtual attribute till we switch completely to polymorphic assignee
def assignee_type
return 'AgentBot' if assignee_agent_bot_id.present?
return 'User' if assignee_id.present?
nil
end
def assigned_entity
assignee_agent_bot || assignee
end
def tweet? def tweet?
inbox.inbox_type == 'Twitter' && additional_attributes['type'] == 'tweet' inbox.inbox_type == 'Twitter' && additional_attributes['type'] == 'tweet'
end end
@@ -226,6 +241,12 @@ class Conversation < ApplicationRecord
self.additional_attributes = {} unless additional_attributes.is_a?(Hash) self.additional_attributes = {} unless additional_attributes.is_a?(Hash)
end end
def reset_agent_bot_when_assignee_present
return if assignee_id.blank?
self.assignee_agent_bot_id = nil
end
def determine_conversation_status def determine_conversation_status
self.status = :resolved and return if contact.blocked? self.status = :resolved and return if contact.blocked?
@@ -251,8 +272,8 @@ class Conversation < ApplicationRecord
end end
def list_of_keys def list_of_keys
%w[team_id assignee_id status snoozed_until custom_attributes label_list waiting_since first_reply_created_at %w[team_id assignee_id assignee_agent_bot_id status snoozed_until custom_attributes label_list waiting_since
priority] first_reply_created_at priority]
end end
def allowed_keys? def allowed_keys?

View File

@@ -30,7 +30,8 @@ class Conversations::EventDataPresenter < SimpleDelegator
def push_meta def push_meta
{ {
sender: contact.push_event_data, sender: contact.push_event_data,
assignee: assignee&.push_event_data, assignee: assigned_entity&.push_event_data,
assignee_type: assignee_type,
team: team&.push_event_data, team: team&.push_event_data,
hmac_verified: contact_inbox&.hmac_verified hmac_verified: contact_inbox&.hmac_verified
} }

View File

@@ -0,0 +1,43 @@
class Conversations::AssignmentService
def initialize(conversation:, assignee_id:, assignee_type: nil)
@conversation = conversation
@assignee_id = assignee_id
@assignee_type = assignee_type
end
def perform
agent_bot_assignment? ? assign_agent_bot : assign_agent
end
private
attr_reader :conversation, :assignee_id, :assignee_type
def assign_agent
conversation.assignee = assignee
conversation.assignee_agent_bot = nil
conversation.save!
assignee
end
def assign_agent_bot
return unless agent_bot
conversation.assignee = nil
conversation.assignee_agent_bot = agent_bot
conversation.save!
agent_bot
end
def assignee
@assignee ||= conversation.account.users.find_by(id: assignee_id)
end
def agent_bot
@agent_bot ||= AgentBot.accessible_to(conversation.account).find_by(id: assignee_id)
end
def agent_bot_assignment?
assignee_type.to_s == 'AgentBot'
end
end

View File

@@ -1,4 +0,0 @@
json.payload do
json.assignee @conversation.assignee
json.conversation_id @conversation.display_id
end

View File

@@ -7,10 +7,16 @@ json.meta do
json.partial! 'api/v1/models/contact', formats: [:json], resource: conversation.contact json.partial! 'api/v1/models/contact', formats: [:json], resource: conversation.contact
end end
json.channel conversation.inbox.try(:channel_type) json.channel conversation.inbox.try(:channel_type)
if conversation.assignee&.account if conversation.assigned_entity.is_a?(AgentBot)
json.assignee do json.assignee do
json.partial! 'api/v1/models/agent', formats: [:json], resource: conversation.assignee json.partial! 'api/v1/models/agent_bot_slim', formats: [:json], resource: conversation.assigned_entity
end end
json.assignee_type 'AgentBot'
elsif conversation.assigned_entity&.account
json.assignee do
json.partial! 'api/v1/models/agent', formats: [:json], resource: conversation.assigned_entity
end
json.assignee_type 'User'
end end
if conversation.team.present? if conversation.team.present?
json.team do json.team do

View File

@@ -0,0 +1,6 @@
json.id resource.id
json.name resource.name
json.description resource.description
json.thumbnail resource.avatar_url
json.outgoing_url resource.outgoing_url unless resource.system_bot?
json.bot_type resource.bot_type

View File

@@ -0,0 +1,5 @@
class AddAssigneeAgentBotIdToConversations < ActiveRecord::Migration[7.1]
def change
add_column :conversations, :assignee_agent_bot_id, :bigint
end
end

View File

@@ -668,6 +668,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_11_14_173609) do
t.bigint "sla_policy_id" t.bigint "sla_policy_id"
t.datetime "waiting_since" t.datetime "waiting_since"
t.text "cached_label_list" t.text "cached_label_list"
t.bigint "assignee_agent_bot_id"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id" t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id"
t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx" t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx"

View File

@@ -15,7 +15,7 @@ RSpec.describe 'Conversation Assignment API', type: :request do
end end
context 'when it is an authenticated bot with out access to the inbox' do context 'when it is an authenticated bot with out access to the inbox' do
let(:agent_bot) { create(:agent_bot, account: account) } let(:agent_bot) { create(:agent_bot) }
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
before do before do
@@ -36,6 +36,7 @@ RSpec.describe 'Conversation Assignment API', type: :request do
context 'when it is an authenticated user with access to the inbox' do context 'when it is an authenticated user with access to the inbox' do
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
let(:agent_bot) { create(:agent_bot, account: account) }
let(:team) { create(:team, account: account) } let(:team) { create(:team, account: account) }
before do before do
@@ -54,6 +55,25 @@ RSpec.describe 'Conversation Assignment API', type: :request do
expect(conversation.reload.assignee).to eq(agent) expect(conversation.reload.assignee).to eq(agent)
end end
it 'assigns an agent bot to the conversation' do
params = { assignee_id: agent_bot.id, assignee_type: 'AgentBot' }
expect(Conversations::AssignmentService).to receive(:new)
.with(hash_including(conversation: conversation, assignee_id: agent_bot.id, assignee_type: 'AgentBot'))
.and_call_original
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['name']).to eq(agent_bot.name)
conversation.reload
expect(conversation.assignee_agent_bot).to eq(agent_bot)
expect(conversation.assignee).to be_nil
end
it 'assigns a team to the conversation' do it 'assigns a team to the conversation' do
team_member = create(:user, account: account, role: :agent, auto_offline: false) team_member = create(:user, account: account, role: :agent, auto_offline: false)
create(:inbox_member, inbox: conversation.inbox, user: team_member) create(:inbox_member, inbox: conversation.inbox, user: team_member)
@@ -125,7 +145,7 @@ RSpec.describe 'Conversation Assignment API', type: :request do
end end
it 'unassigns the assignee from the conversation' do it 'unassigns the assignee from the conversation' do
params = { assignee_id: 0 } params = { assignee_id: nil }
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id), post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
params: params, params: params,
headers: agent.create_new_auth_token, headers: agent.create_new_auth_token,

View File

@@ -36,6 +36,24 @@ describe AgentBotListener do
expect(AgentBots::WebhookJob).not_to receive(:perform_later) expect(AgentBots::WebhookJob).not_to receive(:perform_later)
listener.message_created(event) listener.message_created(event)
end end
context 'when conversation has a different assignee agent bot' do
let!(:conversation_bot) { create(:agent_bot) }
before do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
conversation.update!(assignee_agent_bot: conversation_bot, assignee: nil)
end
it 'sends message to both bots exactly once' do
payload = message.webhook_data.merge(event: 'message_created')
expect(AgentBots::WebhookJob).to receive(:perform_later).with(agent_bot.outgoing_url, payload).once
expect(AgentBots::WebhookJob).to receive(:perform_later).with(conversation_bot.outgoing_url, payload).once
listener.message_created(event)
end
end
end end
end end

View File

@@ -525,8 +525,9 @@ RSpec.describe Conversation do
additional_attributes: {}, additional_attributes: {},
meta: { meta: {
sender: conversation.contact.push_event_data, sender: conversation.contact.push_event_data,
assignee: conversation.assignee, assignee: conversation.assigned_entity&.push_event_data,
team: conversation.team, assignee_type: conversation.assignee_type,
team: conversation.team&.push_event_data,
hmac_verified: conversation.contact_inbox.hmac_verified hmac_verified: conversation.contact_inbox.hmac_verified
}, },
id: conversation.display_id, id: conversation.display_id,

View File

@@ -12,8 +12,9 @@ RSpec.describe Conversations::EventDataPresenter do
additional_attributes: {}, additional_attributes: {},
meta: { meta: {
sender: conversation.contact.push_event_data, sender: conversation.contact.push_event_data,
assignee: conversation.assignee, assignee: conversation.assigned_entity&.push_event_data,
team: conversation.team, assignee_type: conversation.assignee_type,
team: conversation.team&.push_event_data,
hmac_verified: conversation.contact_inbox.hmac_verified hmac_verified: conversation.contact_inbox.hmac_verified
}, },
id: conversation.display_id, id: conversation.display_id,

View File

@@ -0,0 +1,63 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::BaseController', type: :request do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let!(:conversation) { create(:conversation, account: account, inbox: inbox) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: inbox, user: agent)
end
context 'when agent bot belongs to the account' do
let(:agent_bot) { create(:agent_bot, account: account) }
it 'allows assignments via API' do
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
headers: { api_access_token: agent_bot.access_token.token },
params: { assignee_id: agent.id },
as: :json
expect(response).to have_http_status(:success)
end
end
context 'when agent bot belongs to another account' do
let(:other_account) { create(:account) }
let(:external_bot) { create(:agent_bot, account: other_account) }
it 'rejects assignment' do
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
headers: { api_access_token: external_bot.access_token.token },
params: { assignee_id: agent.id },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when agent bot is global' do
let(:global_bot) { create(:agent_bot, account: nil) }
it 'rejects requests without inbox mapping' do
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
headers: { api_access_token: global_bot.access_token.token },
params: { assignee_id: agent.id },
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'allows requests when inbox mapping exists' do
create(:agent_bot_inbox, agent_bot: global_bot, inbox: inbox)
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
headers: { api_access_token: global_bot.access_token.token },
params: { assignee_id: agent.id },
as: :json
expect(response).to have_http_status(:success)
end
end
end

View File

@@ -0,0 +1,60 @@
require 'rails_helper'
describe Conversations::AssignmentService do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account) }
let(:agent_bot) { create(:agent_bot, account: account) }
let(:conversation) { create(:conversation, account: account) }
describe '#perform' do
context 'when assignee_id is blank' do
before do
conversation.update!(assignee: agent, assignee_agent_bot: agent_bot)
end
it 'clears both human and bot assignees' do
described_class.new(conversation: conversation, assignee_id: nil).perform
conversation.reload
expect(conversation.assignee_id).to be_nil
expect(conversation.assignee_agent_bot_id).to be_nil
end
end
context 'when assigning a user' do
before do
conversation.update!(assignee_agent_bot: agent_bot, assignee: nil)
end
it 'sets the agent and clears agent bot' do
result = described_class.new(conversation: conversation, assignee_id: agent.id).perform
conversation.reload
expect(result).to eq(agent)
expect(conversation.assignee_id).to eq(agent.id)
expect(conversation.assignee_agent_bot_id).to be_nil
end
end
context 'when assigning an agent bot' do
let(:service) do
described_class.new(
conversation: conversation,
assignee_id: agent_bot.id,
assignee_type: 'AgentBot'
)
end
it 'sets the agent bot and clears human assignee' do
conversation.update!(assignee: agent, assignee_agent_bot: nil)
result = service.perform
conversation.reload
expect(result).to eq(agent_bot)
expect(conversation.assignee_agent_bot_id).to eq(agent_bot.id)
expect(conversation.assignee_id).to be_nil
end
end
end
end