From 2acb48bbe0cd3a98980593bfe321f428a7ddf35d Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Thu, 21 Apr 2022 20:52:23 +0530 Subject: [PATCH] feat: Add support for conversation attribute automation conditions in message event (#4518) Co-authored-by: Pranav Raj S --- app/builders/messages/message_builder.rb | 7 +- .../settings/automation/constants.js | 21 ++ app/listeners/automation_rule_listener.rb | 2 +- app/models/automation_rule.rb | 2 +- .../automation_rules/action_service.rb | 2 +- .../conditions_filter_service.rb | 63 +++--- .../automation_rule_listener_spec.rb | 195 +++++++++++++++++- 7 files changed, 255 insertions(+), 37 deletions(-) diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 9c26ccca1..d2936e9f3 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -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 diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js index bdec6ebc0..3d1dac355 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -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: [ { diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb index 6ebc46666..0f445fea7 100644 --- a/app/listeners/automation_rule_listener.rb +++ b/app/listeners/automation_rule_listener.rb @@ -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 diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb index 1dcc2583d..8211de98b 100644 --- a/app/models/automation_rule.rb +++ b/app/models/automation_rule.rb @@ -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 diff --git a/app/services/automation_rules/action_service.rb b/app/services/automation_rules/action_service.rb index 0771f817d..887e82b38 100644 --- a/app/services/automation_rules/action_service.rb +++ b/app/services/automation_rules/action_service.rb @@ -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 diff --git a/app/services/automation_rules/conditions_filter_service.rb b/app/services/automation_rules/conditions_filter_service.rb index 2ac011fbd..67cd2a62c 100644 --- a/app/services/automation_rules/conditions_filter_service.rb +++ b/app/services/automation_rules/conditions_filter_service.rb @@ -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 diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb index 6bbffe077..079b9bbd5 100644 --- a/spec/listeners/automation_rule_listener_spec.rb +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -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