Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4dba113ce |
@@ -1,8 +1,8 @@
|
|||||||
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/bot to a conversation
|
||||||
def create
|
def create
|
||||||
if params.key?(:assignee_id)
|
if params.key?(:assignee_id)
|
||||||
set_agent
|
set_assignee
|
||||||
elsif params.key?(:team_id)
|
elsif params.key?(:team_id)
|
||||||
set_team
|
set_team
|
||||||
else
|
else
|
||||||
@@ -12,18 +12,26 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_agent
|
def set_assignee
|
||||||
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
@assignee = case params[:assignee_type]
|
||||||
@conversation.assignee = @agent
|
when 'AgentBot'
|
||||||
|
Current.account.agent_bots.find_by(id: params[:assignee_id])
|
||||||
|
else
|
||||||
|
Current.account.users.find_by(id: params[:assignee_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
@conversation.assignee = @assignee
|
||||||
@conversation.save!
|
@conversation.save!
|
||||||
render_agent
|
render_assignee
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_agent
|
def render_assignee
|
||||||
if @agent.nil?
|
if @assignee.nil?
|
||||||
render json: nil
|
render json: nil
|
||||||
|
elsif @assignee.is_a?(AgentBot)
|
||||||
|
render json: @assignee.webhook_data
|
||||||
else
|
else
|
||||||
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent }
|
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @assignee }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ class AgentBotListener < BaseListener
|
|||||||
process_webhook_bot_event(inbox.agent_bot, payload)
|
process_webhook_bot_event(inbox.agent_bot, payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assignee_changed(event)
|
||||||
|
conversation = extract_conversation_and_account(event)[0]
|
||||||
|
return unless conversation.assigned_to_bot?
|
||||||
|
|
||||||
|
agent_bot = conversation.assigned_bot
|
||||||
|
return if agent_bot.blank? || agent_bot.outgoing_url.blank?
|
||||||
|
|
||||||
|
event_name = __method__.to_s
|
||||||
|
payload = conversation.webhook_data.merge(event: event_name)
|
||||||
|
process_webhook_bot_event(agent_bot, payload)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def connected_agent_bot_exist?(inbox)
|
def connected_agent_bot_exist?(inbox)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class AgentBot < ApplicationRecord
|
|||||||
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, as: :assignee, class_name: 'Conversation', dependent: :nullify
|
||||||
belongs_to :account, optional: true
|
belongs_to :account, optional: true
|
||||||
enum bot_type: { webhook: 0 }
|
enum bot_type: { webhook: 0 }
|
||||||
|
|
||||||
|
|||||||
+42
-18
@@ -6,6 +6,7 @@
|
|||||||
# additional_attributes :jsonb
|
# additional_attributes :jsonb
|
||||||
# agent_last_seen_at :datetime
|
# agent_last_seen_at :datetime
|
||||||
# assignee_last_seen_at :datetime
|
# assignee_last_seen_at :datetime
|
||||||
|
# assignee_type :string
|
||||||
# cached_label_list :text
|
# cached_label_list :text
|
||||||
# contact_last_seen_at :datetime
|
# contact_last_seen_at :datetime
|
||||||
# custom_attributes :jsonb
|
# custom_attributes :jsonb
|
||||||
@@ -31,22 +32,23 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# conv_acid_inbid_stat_asgnid_idx (account_id,inbox_id,status,assignee_id)
|
# conv_acid_inbid_stat_asgnid_idx (account_id,inbox_id,status,assignee_id)
|
||||||
# index_conversations_on_account_id (account_id)
|
# index_conversations_on_account_id (account_id)
|
||||||
# index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE
|
# index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE
|
||||||
# index_conversations_on_assignee_id_and_account_id (assignee_id,account_id)
|
# index_conversations_on_assignee_id_and_account_id (assignee_id,account_id)
|
||||||
# index_conversations_on_campaign_id (campaign_id)
|
# index_conversations_on_assignee_type_and_assignee_id (assignee_type,assignee_id)
|
||||||
# index_conversations_on_contact_id (contact_id)
|
# index_conversations_on_campaign_id (campaign_id)
|
||||||
# index_conversations_on_contact_inbox_id (contact_inbox_id)
|
# index_conversations_on_contact_id (contact_id)
|
||||||
# index_conversations_on_first_reply_created_at (first_reply_created_at)
|
# index_conversations_on_contact_inbox_id (contact_inbox_id)
|
||||||
# index_conversations_on_id_and_account_id (account_id,id)
|
# index_conversations_on_first_reply_created_at (first_reply_created_at)
|
||||||
# index_conversations_on_inbox_id (inbox_id)
|
# index_conversations_on_id_and_account_id (account_id,id)
|
||||||
# index_conversations_on_priority (priority)
|
# index_conversations_on_inbox_id (inbox_id)
|
||||||
# index_conversations_on_status_and_account_id (status,account_id)
|
# index_conversations_on_priority (priority)
|
||||||
# index_conversations_on_status_and_priority (status,priority)
|
# index_conversations_on_status_and_account_id (status,account_id)
|
||||||
# index_conversations_on_team_id (team_id)
|
# index_conversations_on_status_and_priority (status,priority)
|
||||||
# index_conversations_on_uuid (uuid) UNIQUE
|
# index_conversations_on_team_id (team_id)
|
||||||
# index_conversations_on_waiting_since (waiting_since)
|
# index_conversations_on_uuid (uuid) UNIQUE
|
||||||
|
# index_conversations_on_waiting_since (waiting_since)
|
||||||
#
|
#
|
||||||
|
|
||||||
class Conversation < ApplicationRecord
|
class Conversation < ApplicationRecord
|
||||||
@@ -59,6 +61,7 @@ class Conversation < ApplicationRecord
|
|||||||
include SortHandler
|
include SortHandler
|
||||||
include PushDataHelper
|
include PushDataHelper
|
||||||
include ConversationMuteHelpers
|
include ConversationMuteHelpers
|
||||||
|
include Events::Types
|
||||||
|
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :inbox_id, presence: true
|
validates :inbox_id, presence: true
|
||||||
@@ -68,6 +71,7 @@ class Conversation < ApplicationRecord
|
|||||||
validates :custom_attributes, jsonb_attributes_length: true
|
validates :custom_attributes, jsonb_attributes_length: true
|
||||||
validates :uuid, uniqueness: true
|
validates :uuid, uniqueness: true
|
||||||
validate :validate_referer_url
|
validate :validate_referer_url
|
||||||
|
validate :validate_assignee_belongs_to_account
|
||||||
|
|
||||||
enum status: { open: 0, resolved: 1, pending: 2, snoozed: 3 }
|
enum status: { open: 0, resolved: 1, pending: 2, snoozed: 3 }
|
||||||
enum priority: { low: 0, medium: 1, high: 2, urgent: 3 }
|
enum priority: { low: 0, medium: 1, high: 2, urgent: 3 }
|
||||||
@@ -96,7 +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, polymorphic: true, 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
|
||||||
@@ -195,6 +199,14 @@ class Conversation < ApplicationRecord
|
|||||||
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assigned_to_bot?
|
||||||
|
assignee_type == 'AgentBot'
|
||||||
|
end
|
||||||
|
|
||||||
|
def assigned_bot
|
||||||
|
assignee if assigned_to_bot?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def execute_after_update_commit_callbacks
|
def execute_after_update_commit_callbacks
|
||||||
@@ -273,7 +285,8 @@ class Conversation < ApplicationRecord
|
|||||||
CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? },
|
CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? },
|
||||||
CONVERSATION_STATUS_CHANGED => -> { saved_change_to_status? },
|
CONVERSATION_STATUS_CHANGED => -> { saved_change_to_status? },
|
||||||
CONVERSATION_READ => -> { saved_change_to_contact_last_seen_at? },
|
CONVERSATION_READ => -> { saved_change_to_contact_last_seen_at? },
|
||||||
CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? }
|
CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? },
|
||||||
|
ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? }
|
||||||
}.each do |event, condition|
|
}.each do |event, condition|
|
||||||
condition.call && dispatcher_dispatch(event, status_change)
|
condition.call && dispatcher_dispatch(event, status_change)
|
||||||
end
|
end
|
||||||
@@ -309,6 +322,17 @@ class Conversation < ApplicationRecord
|
|||||||
self['additional_attributes']['referer'] = nil unless url_valid?(additional_attributes['referer'])
|
self['additional_attributes']['referer'] = nil unless url_valid?(additional_attributes['referer'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_assignee_belongs_to_account
|
||||||
|
return unless assignee
|
||||||
|
|
||||||
|
case assignee
|
||||||
|
when User
|
||||||
|
errors.add(:assignee, 'must belong to the same account') unless assignee.accounts.include?(account)
|
||||||
|
when AgentBot
|
||||||
|
errors.add(:assignee, 'must belong to the same account') unless assignee.account_id == account_id || assignee.system_bot?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# creating db triggers
|
# creating db triggers
|
||||||
trigger.before(:insert).for_each(:row) do
|
trigger.before(:insert).for_each(:row) do
|
||||||
"NEW.display_id := nextval('conv_dpid_seq_' || NEW.account_id);"
|
"NEW.display_id := nextval('conv_dpid_seq_' || NEW.account_id);"
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ class User < ApplicationRecord
|
|||||||
has_many :accounts, through: :account_users
|
has_many :accounts, through: :account_users
|
||||||
accepts_nested_attributes_for :account_users
|
accepts_nested_attributes_for :account_users
|
||||||
|
|
||||||
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify, inverse_of: :assignee
|
has_many :assigned_conversations, as: :assignee, class_name: 'Conversation', dependent: :nullify
|
||||||
alias_attribute :conversations, :assigned_conversations
|
alias_attribute :conversations, :assigned_conversations
|
||||||
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify, inverse_of: :assigned_agent
|
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify, inverse_of: :assigned_agent
|
||||||
has_many :conversation_participants, dependent: :destroy_async
|
has_many :conversation_participants, dependent: :destroy_async
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class MakeConversationAssigneePolymorphic < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :conversations, :assignee_type, :string
|
||||||
|
add_index :conversations, [:assignee_type, :assignee_id]
|
||||||
|
|
||||||
|
# Update existing records to use User type
|
||||||
|
reversible do |dir|
|
||||||
|
dir.up do
|
||||||
|
execute "UPDATE conversations SET assignee_type = 'User' WHERE assignee_id IS NOT NULL"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+3
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2025_07_10_145708) do
|
ActiveRecord::Schema[7.1].define(version: 2025_07_14_205206) do
|
||||||
# These extensions should be enabled to support this database
|
# These extensions should be enabled to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
@@ -585,11 +585,13 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_10_145708) 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.string "assignee_type"
|
||||||
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"
|
||||||
t.index ["account_id"], name: "index_conversations_on_account_id"
|
t.index ["account_id"], name: "index_conversations_on_account_id"
|
||||||
t.index ["assignee_id", "account_id"], name: "index_conversations_on_assignee_id_and_account_id"
|
t.index ["assignee_id", "account_id"], name: "index_conversations_on_assignee_id_and_account_id"
|
||||||
|
t.index ["assignee_type", "assignee_id"], name: "index_conversations_on_assignee_type_and_assignee_id"
|
||||||
t.index ["campaign_id"], name: "index_conversations_on_campaign_id"
|
t.index ["campaign_id"], name: "index_conversations_on_campaign_id"
|
||||||
t.index ["contact_id"], name: "index_conversations_on_contact_id"
|
t.index ["contact_id"], name: "index_conversations_on_contact_id"
|
||||||
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
|
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ require 'rails_helper'
|
|||||||
|
|
||||||
shared_examples_for 'assignment_handler' do
|
shared_examples_for 'assignment_handler' do
|
||||||
describe '#update_team' do
|
describe '#update_team' do
|
||||||
let(:conversation) { create(:conversation, assignee: create(:user)) }
|
let(:conversation) { create(:conversation) }
|
||||||
|
let(:assignee) { create(:user, account: conversation.account, role: :agent) }
|
||||||
let(:agent) do
|
let(:agent) do
|
||||||
create(:user, email: 'agent@example.com', account: conversation.account, role: :agent, auto_offline: false)
|
create(:user, email: 'agent@example.com', account: conversation.account, role: :agent, auto_offline: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
conversation.update!(assignee: assignee)
|
||||||
|
end
|
||||||
let(:team) do
|
let(:team) do
|
||||||
create(:team, account: conversation.account, allow_auto_assign: false)
|
create(:team, account: conversation.account, allow_auto_assign: false)
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user