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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
43
app/services/conversations/assignment_service.rb
Normal file
43
app/services/conversations/assignment_service.rb
Normal 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
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
json.payload do
|
|
||||||
json.assignee @conversation.assignee
|
|
||||||
json.conversation_id @conversation.display_id
|
|
||||||
end
|
|
||||||
@@ -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
|
||||||
|
|||||||
6
app/views/api/v1/models/_agent_bot_slim.json.jbuilder
Normal file
6
app/views/api/v1/models/_agent_bot_slim.json.jbuilder
Normal 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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddAssigneeAgentBotIdToConversations < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :conversations, :assignee_agent_bot_id, :bigint
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
63
spec/requests/api/v1/accounts/base_controller_spec.rb
Normal file
63
spec/requests/api/v1/accounts/base_controller_spec.rb
Normal 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
|
||||||
60
spec/services/conversations/assignment_service_spec.rb
Normal file
60
spec/services/conversations/assignment_service_spec.rb
Normal 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
|
||||||
Reference in New Issue
Block a user