diff --git a/app/controllers/api/v1/accounts/agent_bots_controller.rb b/app/controllers/api/v1/accounts/agent_bots_controller.rb index 64c35d33d..c2f919659 100644 --- a/app/controllers/api/v1/accounts/agent_bots_controller.rb +++ b/app/controllers/api/v1/accounts/agent_bots_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController before_action :agent_bot, except: [:index, :create] def index - @agent_bots = AgentBot.where(account_id: [nil, Current.account.id]) + @agent_bots = AgentBot.accessible_to(Current.account) end def show; end @@ -37,7 +37,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController private 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]) end diff --git a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb index 1fb2095e3..49806e97c 100644 --- a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb @@ -1,7 +1,7 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController # assigns agent/team to a conversation def create - if params.key?(:assignee_id) + if params.key?(:assignee_id) || agent_bot_assignment? set_agent elsif params.key?(:team_id) set_team @@ -13,17 +13,23 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account private def set_agent - @agent = Current.account.users.find_by(id: params[:assignee_id]) - @conversation.assignee = @agent - @conversation.save! - render_agent + resource = Conversations::AssignmentService.new( + conversation: @conversation, + assignee_id: params[:assignee_id], + assignee_type: params[:assignee_type] + ).perform + + render_agent(resource) end - def render_agent - if @agent.nil? - render json: nil + def render_agent(resource) + case resource + 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 - render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent } + render json: nil end end @@ -32,4 +38,8 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account @conversation.update!(team: @team) render json: @team end + + def agent_bot_assignment? + params[:assignee_type].to_s == 'AgentBot' + end end diff --git a/app/controllers/concerns/ensure_current_account_helper.rb b/app/controllers/concerns/ensure_current_account_helper.rb index 3baf9ee1e..ea36a48f2 100644 --- a/app/controllers/concerns/ensure_current_account_helper.rb +++ b/app/controllers/concerns/ensure_current_account_helper.rb @@ -25,6 +25,9 @@ module EnsureCurrentAccountHelper end 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 diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 0f539bfa9..f94fca452 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -63,10 +63,9 @@ class ConversationApi extends ApiClient { } assignAgent({ conversationId, agentId }) { - return axios.post( - `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, - {} - ); + return axios.post(`${this.url}/${conversationId}/assignments`, { + assignee_id: agentId, + }); } assignTeam({ conversationId, teamId }) { diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index dd1615802..de0d7a7d0 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -92,8 +92,10 @@ describe('#ConversationAPI', () => { it('#assignAgent', () => { conversationAPI.assignAgent({ conversationId: 12, agentId: 34 }); expect(axiosMock.post).toHaveBeenCalledWith( - `/api/v1/conversations/12/assignments?assignee_id=34`, - {} + `/api/v1/conversations/12/assignments`, + { + assignee_id: 34, + } ); }); diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBoxBanner.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBoxBanner.vue index 3aea41f90..d80910343 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBoxBanner.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBoxBanner.vue @@ -30,7 +30,7 @@ const assignedAgent = computed({ return currentChat.value?.meta?.assignee; }, set(agent) { - const agentId = agent ? agent.id : 0; + const agentId = agent ? agent.id : null; store.dispatch('setCurrentChatAssignee', agent); store.dispatch('assignAgent', { conversationId: currentChat.value?.id, diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue index f94fc7a9b..76533a94d 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue @@ -84,7 +84,7 @@ export default { return this.currentChat.meta.assignee; }, set(agent) { - const agentId = agent ? agent.id : 0; + const agentId = agent ? agent.id : null; this.$store.dispatch('setCurrentChatAssignee', agent); this.$store .dispatch('assignAgent', { diff --git a/app/listeners/agent_bot_listener.rb b/app/listeners/agent_bot_listener.rb index 38c046e34..ccac8005f 100644 --- a/app/listeners/agent_bot_listener.rb +++ b/app/listeners/agent_bot_listener.rb @@ -2,61 +2,60 @@ class AgentBotListener < BaseListener def conversation_resolved(event) conversation = extract_conversation_and_account(event)[0] inbox = conversation.inbox - return unless connected_agent_bot_exist?(inbox) - event_name = __method__.to_s 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 def conversation_opened(event) conversation = extract_conversation_and_account(event)[0] inbox = conversation.inbox - return unless connected_agent_bot_exist?(inbox) - event_name = __method__.to_s 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 def message_created(event) message = extract_message_and_account(event)[0] inbox = message.inbox - return unless connected_agent_bot_exist?(inbox) return unless message.webhook_sendable? 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 def message_updated(event) message = extract_message_and_account(event)[0] inbox = message.inbox - return unless connected_agent_bot_exist?(inbox) return unless message.webhook_sendable? 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 def webwidget_triggered(event) contact_inbox = event.data[:contact_inbox] inbox = contact_inbox.inbox - return unless connected_agent_bot_exist?(inbox) - event_name = __method__.to_s payload = contact_inbox.webhook_data.merge(event: event_name) 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 private - def connected_agent_bot_exist?(inbox) - return if inbox.agent_bot_inbox.blank? - return unless inbox.agent_bot_inbox.active? + def agent_bots_for(inbox, conversation = nil) + bots = [] + 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 def process_message_event(method_name, agent_bot, message, _event) diff --git a/app/models/agent_bot.rb b/app/models/agent_bot.rb index be63aebc5..b839f21b4 100644 --- a/app/models/agent_bot.rb +++ b/app/models/agent_bot.rb @@ -21,9 +21,18 @@ class AgentBot < ApplicationRecord include AccessTokenable 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 :inboxes, through: :agent_bot_inboxes 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 enum bot_type: { webhook: 0 } diff --git a/app/models/conversation.rb b/app/models/conversation.rb index ca8a3258e..ac0985416 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -20,6 +20,7 @@ # created_at :datetime not null # updated_at :datetime not null # account_id :integer not null +# assignee_agent_bot_id :bigint # assignee_id :integer # campaign_id :bigint # contact_id :bigint @@ -65,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 @@ -98,6 +100,7 @@ class Conversation < ApplicationRecord belongs_to :account belongs_to :inbox 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_inbox belongs_to :team, optional: true @@ -180,6 +183,18 @@ class Conversation < ApplicationRecord true 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? inbox.inbox_type == 'Twitter' && additional_attributes['type'] == 'tweet' end @@ -226,6 +241,12 @@ class Conversation < ApplicationRecord self.additional_attributes = {} unless additional_attributes.is_a?(Hash) end + def reset_agent_bot_when_assignee_present + return if assignee_id.blank? + + self.assignee_agent_bot_id = nil + end + def determine_conversation_status self.status = :resolved and return if contact.blocked? @@ -251,8 +272,8 @@ class Conversation < ApplicationRecord end def list_of_keys - %w[team_id assignee_id status snoozed_until custom_attributes label_list waiting_since first_reply_created_at - priority] + %w[team_id assignee_id assignee_agent_bot_id status snoozed_until custom_attributes label_list waiting_since + first_reply_created_at priority] end def allowed_keys? diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb index 2ef69080d..4a9216b05 100644 --- a/app/presenters/conversations/event_data_presenter.rb +++ b/app/presenters/conversations/event_data_presenter.rb @@ -30,7 +30,8 @@ class Conversations::EventDataPresenter < SimpleDelegator def push_meta { 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, hmac_verified: contact_inbox&.hmac_verified } diff --git a/app/services/conversations/assignment_service.rb b/app/services/conversations/assignment_service.rb new file mode 100644 index 000000000..adca34f06 --- /dev/null +++ b/app/services/conversations/assignment_service.rb @@ -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 diff --git a/app/views/api/v1/accounts/conversations/assignments/create.json.jbuilder b/app/views/api/v1/accounts/conversations/assignments/create.json.jbuilder deleted file mode 100644 index 1c5bd4846..000000000 --- a/app/views/api/v1/accounts/conversations/assignments/create.json.jbuilder +++ /dev/null @@ -1,4 +0,0 @@ -json.payload do - json.assignee @conversation.assignee - json.conversation_id @conversation.display_id -end diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder index 8867ba695..4cb13f543 100644 --- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder +++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder @@ -7,10 +7,16 @@ json.meta do json.partial! 'api/v1/models/contact', formats: [:json], resource: conversation.contact end json.channel conversation.inbox.try(:channel_type) - if conversation.assignee&.account + if conversation.assigned_entity.is_a?(AgentBot) 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 + 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 if conversation.team.present? json.team do diff --git a/app/views/api/v1/models/_agent_bot_slim.json.jbuilder b/app/views/api/v1/models/_agent_bot_slim.json.jbuilder new file mode 100644 index 000000000..29f1e12a5 --- /dev/null +++ b/app/views/api/v1/models/_agent_bot_slim.json.jbuilder @@ -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 diff --git a/db/migrate/20251022162159_add_assignee_agent_bot_id_to_conversations.rb b/db/migrate/20251022162159_add_assignee_agent_bot_id_to_conversations.rb new file mode 100644 index 000000000..0a96c319e --- /dev/null +++ b/db/migrate/20251022162159_add_assignee_agent_bot_id_to_conversations.rb @@ -0,0 +1,5 @@ +class AddAssigneeAgentBotIdToConversations < ActiveRecord::Migration[7.1] + def change + add_column :conversations, :assignee_agent_bot_id, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index 8a6e6676c..b69b5f61b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -668,6 +668,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_11_14_173609) do t.bigint "sla_policy_id" t.datetime "waiting_since" 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", "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" diff --git a/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb index 8ed70b7a2..18c652c0a 100644 --- a/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Conversation Assignment API', type: :request do end 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) } 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 let(:agent) { create(:user, account: account, role: :agent) } + let(:agent_bot) { create(:agent_bot, account: account) } let(:team) { create(:team, account: account) } before do @@ -54,6 +55,25 @@ RSpec.describe 'Conversation Assignment API', type: :request do expect(conversation.reload.assignee).to eq(agent) 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 team_member = create(:user, account: account, role: :agent, auto_offline: false) create(:inbox_member, inbox: conversation.inbox, user: team_member) @@ -125,7 +145,7 @@ RSpec.describe 'Conversation Assignment API', type: :request do end 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), params: params, headers: agent.create_new_auth_token, diff --git a/spec/listeners/agent_bot_listener_spec.rb b/spec/listeners/agent_bot_listener_spec.rb index 2dc2b41df..56c478a1b 100644 --- a/spec/listeners/agent_bot_listener_spec.rb +++ b/spec/listeners/agent_bot_listener_spec.rb @@ -36,6 +36,24 @@ describe AgentBotListener do expect(AgentBots::WebhookJob).not_to receive(:perform_later) listener.message_created(event) 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 diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 51bb43384..7a5398456 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -525,8 +525,9 @@ RSpec.describe Conversation do additional_attributes: {}, meta: { sender: conversation.contact.push_event_data, - assignee: conversation.assignee, - team: conversation.team, + assignee: conversation.assigned_entity&.push_event_data, + assignee_type: conversation.assignee_type, + team: conversation.team&.push_event_data, hmac_verified: conversation.contact_inbox.hmac_verified }, id: conversation.display_id, diff --git a/spec/presenters/conversations/event_data_presenter_spec.rb b/spec/presenters/conversations/event_data_presenter_spec.rb index a645caf1d..bf0eaf6f6 100644 --- a/spec/presenters/conversations/event_data_presenter_spec.rb +++ b/spec/presenters/conversations/event_data_presenter_spec.rb @@ -12,8 +12,9 @@ RSpec.describe Conversations::EventDataPresenter do additional_attributes: {}, meta: { sender: conversation.contact.push_event_data, - assignee: conversation.assignee, - team: conversation.team, + assignee: conversation.assigned_entity&.push_event_data, + assignee_type: conversation.assignee_type, + team: conversation.team&.push_event_data, hmac_verified: conversation.contact_inbox.hmac_verified }, id: conversation.display_id, diff --git a/spec/requests/api/v1/accounts/base_controller_spec.rb b/spec/requests/api/v1/accounts/base_controller_spec.rb new file mode 100644 index 000000000..6493bc986 --- /dev/null +++ b/spec/requests/api/v1/accounts/base_controller_spec.rb @@ -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 diff --git a/spec/services/conversations/assignment_service_spec.rb b/spec/services/conversations/assignment_service_spec.rb new file mode 100644 index 000000000..899236e1d --- /dev/null +++ b/spec/services/conversations/assignment_service_spec.rb @@ -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