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:
Tejaswini Chile 2022-04-21 20:52:23 +05:30 committed by GitHub
parent b082b0e58c
commit 2acb48bbe0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 255 additions and 37 deletions

View file

@ -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

View file

@ -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: [
{

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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