feat: Add support for conversation attribute automation conditions in message event (#4518)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
b082b0e58c
commit
2acb48bbe0
7 changed files with 255 additions and 37 deletions
|
@ -9,6 +9,7 @@ class Messages::MessageBuilder
|
|||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
|
@ -64,6 +65,10 @@ class Messages::MessageBuilder
|
|||
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
||||
end
|
||||
|
||||
def automation_rule_id
|
||||
@automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {}
|
||||
end
|
||||
|
||||
def message_sender
|
||||
return if @params[:sender_type] != 'AgentBot'
|
||||
|
||||
|
@ -82,6 +87,6 @@ class Messages::MessageBuilder
|
|||
items: @items,
|
||||
in_reply_to: @in_reply_to,
|
||||
echo_id: @params[:echo_id]
|
||||
}.merge(external_created_at)
|
||||
}.merge(external_created_at).merge(automation_rule_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -64,6 +64,13 @@ export const AUTOMATIONS = {
|
|||
inputType: 'plain_text',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
@ -149,6 +156,13 @@ export const AUTOMATIONS = {
|
|||
inputType: 'plain_text',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
@ -247,6 +261,13 @@ export const AUTOMATIONS = {
|
|||
inputType: 'search_select',
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
|
|
@ -36,7 +36,7 @@ class AutomationRuleListener < BaseListener
|
|||
return unless rule_present?('message_created', account)
|
||||
|
||||
@rules.each do |rule|
|
||||
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation, { message: message }).message_conditions
|
||||
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation, { message: message }).perform
|
||||
::AutomationRules::ActionService.new(rule, account, message.conversation).perform if conditions_match.present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,7 +27,7 @@ class AutomationRule < ApplicationRecord
|
|||
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referer city company].freeze
|
||||
CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referer city company inbox_id].freeze
|
||||
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze
|
||||
|
||||
private
|
||||
|
|
|
@ -56,7 +56,7 @@ class AutomationRules::ActionService
|
|||
end
|
||||
|
||||
def send_message(message)
|
||||
params = { content: message[0], private: false }
|
||||
params = { content: message[0], private: false, content_attributes: { automation_rule_id: @rule.id } }
|
||||
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||
mb.perform
|
||||
end
|
||||
|
|
|
@ -14,36 +14,33 @@ class AutomationRules::ConditionsFilterService < FilterService
|
|||
end
|
||||
|
||||
def perform
|
||||
conversation_filters = @filters['conversations']
|
||||
contact_filters = @filters['contacts']
|
||||
@conversation_filters = @filters['conversations']
|
||||
@contact_filters = @filters['contacts']
|
||||
@message_filters = @filters['messages']
|
||||
|
||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
||||
conversation_filter = conversation_filters[query_hash['attribute_key']]
|
||||
contact_filter = contact_filters[query_hash['attribute_key']]
|
||||
|
||||
if conversation_filter
|
||||
@query_string += conversation_query_string('conversations', conversation_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif contact_filter
|
||||
@query_string += conversation_query_string('contacts', contact_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif custom_attribute(query_hash['attribute_key'], @account)
|
||||
# send table name according to attribute key right now we are supporting contact based custom attribute filter
|
||||
@query_string += custom_attribute_query(query_hash.with_indifferent_access, 'contacts', current_index)
|
||||
end
|
||||
apply_filter(query_hash, current_index)
|
||||
end
|
||||
|
||||
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
||||
records.any?
|
||||
end
|
||||
|
||||
def message_conditions
|
||||
message_filters = @filters['messages']
|
||||
def apply_filter(query_hash, current_index)
|
||||
conversation_filter = @conversation_filters[query_hash['attribute_key']]
|
||||
contact_filter = @contact_filters[query_hash['attribute_key']]
|
||||
message_filter = @message_filters[query_hash['attribute_key']]
|
||||
|
||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
||||
current_filter = message_filters[query_hash['attribute_key']]
|
||||
@query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index)
|
||||
if conversation_filter
|
||||
@query_string += conversation_query_string('conversations', conversation_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif contact_filter
|
||||
@query_string += contact_query_string(contact_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif message_filter
|
||||
@query_string += message_query_string(message_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif custom_attribute(query_hash['attribute_key'], @account)
|
||||
# send table name according to attribute key right now we are supporting contact based custom attribute filter
|
||||
@query_string += custom_attribute_query(query_hash.with_indifferent_access, 'contacts', current_index)
|
||||
end
|
||||
records = Message.where(id: @options[:message].id).where(@query_string, @filter_values.with_indifferent_access)
|
||||
records.any?
|
||||
end
|
||||
|
||||
def message_query_string(current_filter, query_hash, current_index)
|
||||
|
@ -59,16 +56,18 @@ class AutomationRules::ConditionsFilterService < FilterService
|
|||
end
|
||||
|
||||
# This will be used in future for contact automation rule
|
||||
def contact_conditions(_contact)
|
||||
conversation_filters = @filters['conversations']
|
||||
def contact_query_string(current_filter, query_hash, current_index)
|
||||
attribute_key = query_hash['attribute_key']
|
||||
query_operator = query_hash['query_operator']
|
||||
|
||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
||||
current_filter = conversation_filters[query_hash['attribute_key']]
|
||||
@query_string += conversation_query_string(current_filter, query_hash.with_indifferent_access, current_index)
|
||||
filter_operator_value = filter_operation(query_hash, current_index)
|
||||
|
||||
case current_filter['attribute_type']
|
||||
when 'additional_attributes'
|
||||
" contacts.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
||||
when 'standard'
|
||||
" contacts.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||
end
|
||||
|
||||
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
||||
records.any?
|
||||
end
|
||||
|
||||
def conversation_query_string(table_name, current_filter, query_hash, current_index)
|
||||
|
@ -91,6 +90,12 @@ class AutomationRules::ConditionsFilterService < FilterService
|
|||
private
|
||||
|
||||
def base_relation
|
||||
Conversation.where(id: @conversation.id).joins('LEFT OUTER JOIN contacts on conversations.contact_id = contacts.id')
|
||||
records = Conversation.where(id: @conversation.id).joins(
|
||||
'LEFT OUTER JOIN contacts on conversations.contact_id = contacts.id'
|
||||
).joins(
|
||||
'LEFT OUTER JOIN messages on messages.conversation_id = conversations.id'
|
||||
)
|
||||
records = records.where(messages: { id: @options[:message].id }) if @options[:message].present?
|
||||
records
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,9 +10,6 @@ describe AutomationRuleListener do
|
|||
let(:team) { create(:team, account: account) }
|
||||
let(:user_1) { create(:user, role: 0) }
|
||||
let(:user_2) { create(:user, role: 0) }
|
||||
let!(:event) do
|
||||
Events::Base.new('conversation_status_changed', Time.zone.now, { conversation: conversation })
|
||||
end
|
||||
|
||||
before do
|
||||
create(:custom_attribute_definition,
|
||||
|
@ -39,7 +36,7 @@ describe AutomationRuleListener do
|
|||
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
|
||||
{ 'action_name' => 'send_webhook_event', 'action_params' => ['https://www.example.com'] },
|
||||
{ 'action_name' => 'assign_best_agent', 'action_params' => [user_1.id] },
|
||||
{ 'action_name' => 'send_email_transcript', 'action_params' => 'new_agent@example.com' },
|
||||
{ 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] },
|
||||
{ 'action_name' => 'mute_conversation', 'action_params' => nil },
|
||||
{ 'action_name' => 'change_status', 'action_params' => ['snoozed'] },
|
||||
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||
|
@ -70,6 +67,12 @@ describe AutomationRuleListener do
|
|||
attribute_key: 'customer_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['platinum'],
|
||||
query_operator: 'AND'
|
||||
}.with_indifferent_access,
|
||||
{
|
||||
attribute_key: 'inbox_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: [inbox.id],
|
||||
query_operator: nil
|
||||
}.with_indifferent_access
|
||||
]
|
||||
|
@ -150,6 +153,40 @@ describe AutomationRuleListener do
|
|||
expect(conversation.team_id).to eq(team.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox condition does not match' do
|
||||
let!(:inbox_1) { create(:inbox, account: account) }
|
||||
let!(:event) do
|
||||
Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation })
|
||||
end
|
||||
|
||||
before do
|
||||
automation_rule.update!(
|
||||
event_name: 'conversation_updated',
|
||||
name: 'Call actions conversation updated',
|
||||
description: 'Add labels, assign team after conversation updated',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'inbox_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: [inbox_1.id],
|
||||
query_operator: nil
|
||||
}.with_indifferent_access
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
it 'triggers automation rule would not send message to the contacts' do
|
||||
expect(conversation.messages.count).to eq(0)
|
||||
expect(conversation.messages).to be_empty
|
||||
|
||||
listener.conversation_updated(event)
|
||||
|
||||
conversation.reload
|
||||
|
||||
expect(conversation.messages.count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#conversation_updated' do
|
||||
|
@ -305,4 +342,154 @@ describe AutomationRuleListener do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#message_created with conversation and contacts based conditions' do
|
||||
before do
|
||||
automation_rule.update!(
|
||||
event_name: 'message_created',
|
||||
name: 'Call actions message created',
|
||||
description: 'Send Message in the conversation',
|
||||
conditions: [
|
||||
{ attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access,
|
||||
{ attribute_key: 'message_type', filter_operator: 'equal_to', values: ['incoming'], query_operator: 'AND' }.with_indifferent_access,
|
||||
{ attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access,
|
||||
{ attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access
|
||||
],
|
||||
actions: [
|
||||
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||
{ 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] }
|
||||
]
|
||||
)
|
||||
conversation.update!(team_id: team.id)
|
||||
conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' })
|
||||
end
|
||||
|
||||
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') }
|
||||
let!(:event) do
|
||||
Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message })
|
||||
end
|
||||
|
||||
context 'when rule matches' do
|
||||
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||
mailer = double
|
||||
|
||||
automation_rule
|
||||
|
||||
listener.message_created(event)
|
||||
|
||||
conversation.reload
|
||||
|
||||
allow(mailer).to receive(:conversation_transcript)
|
||||
end
|
||||
|
||||
it 'triggers automation rule send message to the contacts' do
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
|
||||
listener.message_created(event)
|
||||
|
||||
conversation.reload
|
||||
|
||||
expect(conversation.messages.count).to eq(2)
|
||||
expect(conversation.messages.last.content).to eq('Send this message.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rule does not match' do
|
||||
before do
|
||||
conversation.update!(team_id: team.id)
|
||||
conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' })
|
||||
end
|
||||
|
||||
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') }
|
||||
let!(:event) do
|
||||
Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message })
|
||||
end
|
||||
|
||||
it 'triggers automation rule but wont send message' do
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
|
||||
listener.message_created(event)
|
||||
|
||||
conversation.reload
|
||||
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.content).to eq('Incoming Message')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#conversation_created with contacts based conditions' do
|
||||
before do
|
||||
automation_rule.update!(
|
||||
event_name: 'conversation_created',
|
||||
name: 'Call actions message created',
|
||||
description: 'Send Message in the conversation',
|
||||
conditions: [
|
||||
{ attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access,
|
||||
{ attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access,
|
||||
{ attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access
|
||||
],
|
||||
actions: [
|
||||
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||
{ 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] }
|
||||
]
|
||||
)
|
||||
conversation.update!(team_id: team.id)
|
||||
conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' })
|
||||
end
|
||||
|
||||
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') }
|
||||
let!(:event) do
|
||||
Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message })
|
||||
end
|
||||
|
||||
context 'when rule matches' do
|
||||
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||
mailer = double
|
||||
|
||||
automation_rule
|
||||
|
||||
listener.conversation_created(event)
|
||||
|
||||
conversation.reload
|
||||
|
||||
allow(mailer).to receive(:conversation_transcript)
|
||||
end
|
||||
|
||||
it 'triggers automation rule send message to the contacts' do
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
|
||||
listener.conversation_created(event)
|
||||
|
||||
conversation.reload
|
||||
|
||||
expect(conversation.messages.count).to eq(2)
|
||||
expect(conversation.messages.last.content).to eq('Send this message.')
|
||||
expect(conversation.messages.last.content_attributes[:automation_rule_id]).to eq(automation_rule.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rule does not match' do
|
||||
before do
|
||||
conversation.update!(team_id: team.id)
|
||||
conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' })
|
||||
end
|
||||
|
||||
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') }
|
||||
let!(:event) do
|
||||
Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message })
|
||||
end
|
||||
|
||||
it 'triggers automation rule but wont send message' do
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
|
||||
listener.conversation_created(event)
|
||||
|
||||
conversation.reload
|
||||
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.content).to eq('Incoming Message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue