Automation enhancement (#4087)

This commit is contained in:
Tejaswini Chile 2022-03-21 13:12:27 +05:30 committed by GitHub
parent c608ca37e0
commit e06a6a7c55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 275 additions and 60 deletions

View file

@ -42,7 +42,7 @@ gem 'down', '~> 5.0'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false gem 'azure-storage-blob', require: false
gem 'google-cloud-storage', require: false gem 'google-cloud-storage', require: false
gem 'image_processing' gem 'image_processing', '~> 1.12.2'
##-- gems for database --# ##-- gems for database --#
gem 'groupdate' gem 'groupdate'

View file

@ -688,7 +688,7 @@ DEPENDENCIES
hairtrigger hairtrigger
hashie hashie
html2text html2text
image_processing image_processing (~> 1.12.2)
jbuilder jbuilder
json_refs json_refs
json_schemer json_schemer

View file

@ -34,7 +34,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
params.permit( params.permit(
:name, :description, :event_name, :account_id, :active, :name, :description, :event_name, :account_id, :active,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }], conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
actions: [:action_name, { action_params: [] }] actions: [:action_name, { action_params: [{}] }]
) )
end end

View file

@ -79,7 +79,7 @@ export const AUTOMATIONS = {
// { // {
// key: 'send_email_to_team', // key: 'send_email_to_team',
// name: 'Send an email to team', // name: 'Send an email to team',
// attributeI18nKey: 'SEND_MESSAGE', // attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
// }, // },
], ],
}, },
@ -186,7 +186,7 @@ export const AUTOMATIONS = {
// { // {
// key: 'send_email_to_team', // key: 'send_email_to_team',
// name: 'Send an email to team', // name: 'Send an email to team',
// attributeI18nKey: 'SEND_MESSAGE', // attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
// }, // },
{ {
key: 'assign_agent', key: 'assign_agent',

View file

@ -1,51 +1,58 @@
class AutomationRuleListener < BaseListener class AutomationRuleListener < BaseListener
def conversation_updated(event_obj) def conversation_updated(event_obj)
conversation = event_obj.data[:conversation] conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_updated', conversation) account = conversation.account
return unless rule_present?('conversation_updated', account)
@rules.each do |rule| @rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present? AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
end end
end end
def conversation_status_changed(event_obj) def conversation_status_changed(event_obj)
conversation = event_obj.data[:conversation] conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_status_changed', conversation) account = conversation.account
return unless rule_present?('conversation_status_changed', account)
@rules.each do |rule| @rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present? AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
end end
end end
def conversation_created(event_obj) def conversation_created(event_obj)
conversation = event_obj.data[:conversation] conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_created', conversation) account = conversation.account
return unless rule_present?('conversation_created', account)
@rules.each do |rule| @rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
::AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present? ::AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
end end
end end
def message_created(event_obj) def message_created(event_obj)
message = event_obj.data[:message] message = event_obj.data[:message]
conversation = message.conversation account = message.try(:account)
return unless rule_present?('message_created', conversation)
return unless rule_present?('message_created', account)
@rules.each do |rule| @rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).message_conditions(message) conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation).message_conditions
::AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present? ::AutomationRules::ActionService.new(rule, account, message.conversation).perform if conditions_match.present?
end end
end end
def rule_present?(event_name, conversation) def rule_present?(event_name, account)
return if conversation.blank? return if account.blank?
@rules = AutomationRule.where( @rules = AutomationRule.where(
event_name: event_name, event_name: event_name,
account_id: conversation.account_id, account_id: account.id,
active: true active: true
) )
@rules.any? @rules.any?

View file

@ -4,50 +4,57 @@ class TeamNotifications::AutomationNotificationMailer < ApplicationMailer
@agents = team.team_members @agents = team.team_members
@conversation = conversation @conversation = conversation
@message = message @custom_message = message
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_an_email_to_team send_an_email_to_team and return
end end
def conversation_updated(conversation, team) def conversation_updated(conversation, team, message)
return unless smtp_config_set_or_development? return unless smtp_config_set_or_development?
@agents = team.team_members @agents = team.team_members
@conversation = conversation @conversation = conversation
@message = message @custom_message = message
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_an_email_to_team send_an_email_to_team and return
end end
def message_created(message, agent) def message_created(conversation, team, message)
return unless smtp_config_set_or_development? return unless smtp_config_set_or_development?
@agent = agent @agents = team.team_members
@conversation = message.conversation @conversation = conversation
@message = message @custom_message = message
subject = "#{@agent.available_name}, You have been mentioned in conversation [ID - #{@conversation.display_id}]"
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: @agent.email, subject: subject)
send_an_email_to_team and return
end end
private private
def send_an_email_to_team def send_an_email_to_team
@agents.each do |agent| @agents.each do |agent|
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}." subject = "#{agent.user.available_name}, This email has been sent via automation rule actions."
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: agent.email, subject: subject) @agent = agent
send_mail_with_liquid(to: @agent.user.email, subject: subject)
end end
end end
def liquid_droppables def liquid_droppables
super.merge({ super.merge!({
user: @agent, user: @agent.user,
conversation: @conversation, conversation: @conversation,
inbox: @conversation.inbox, inbox: @conversation.inbox
message: @message })
}) end
def liquid_locals
super.merge!({
custom_message: @custom_message
})
end end
end end

View file

@ -20,9 +20,9 @@
class AutomationRule < ApplicationRecord class AutomationRule < ApplicationRecord
belongs_to :account belongs_to :account
validates :account, presence: true
validate :json_conditions_format validate :json_conditions_format
validate :json_actions_format validate :json_actions_format
validates :account_id, presence: true
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }

View file

@ -210,6 +210,8 @@ class Conversation < ApplicationRecord
end end
def dispatcher_dispatch(event_name) def dispatcher_dispatch(event_name)
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?) Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?)
end end

View file

@ -166,6 +166,8 @@ class Message < ApplicationRecord
end end
def dispatch_create_events def dispatch_create_events
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
Rails.configuration.dispatcher.dispatch(MESSAGE_CREATED, Time.zone.now, message: self) Rails.configuration.dispatcher.dispatch(MESSAGE_CREATED, Time.zone.now, message: self)
if outgoing? && conversation.messages.outgoing.count == 1 if outgoing? && conversation.messages.outgoing.count == 1
@ -174,6 +176,8 @@ class Message < ApplicationRecord
end end
def dispatch_update_event def dispatch_update_event
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self) Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self)
end end

View file

@ -1,40 +1,68 @@
class AutomationRules::ActionService class AutomationRules::ActionService
def initialize(rule, conversation) def initialize(rule, account, conversation)
@rule = rule @rule = rule
@account = account
@conversation = conversation @conversation = conversation
@account = @conversation.account Current.executed_by = rule
end end
def perform def perform
@rule.actions.each do |action, _current_index| @rule.actions.each do |action|
action = action.with_indifferent_access action = action.with_indifferent_access
send(action[:action_name], action[:action_params]) begin
send(action[:action_name], action[:action_params])
rescue StandardError => e
Sentry.capture_exception(e)
end
end end
ensure
Current.reset
end end
private private
def send_email_transcript(email)
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(@conversation, email)&.deliver_later
end
def mute_conversation(_params)
@conversation.mute!
end
def change_status(status)
@conversation.update!(status: status[0])
end
def send_webhook_events(webhook_url)
payload = @conversation.webhook_data.merge(event: "automation_event: #{@rule.event_name}")
WebhookJob.perform_later(webhook_url, payload)
end
def send_message(message) def send_message(message)
# params = { content: message, private: false } return if @rule.event_name == 'message_created'
# mb = Messages::MessageBuilder.new(@administrator, @conversation, params)
# mb.perform params = { content: message[0], private: false }
mb = Messages::MessageBuilder.new(@administrator, @conversation, params)
mb.perform
end end
def assign_team(team_ids = []) def assign_team(team_ids = [])
return unless team_belongs_to_account?(team_ids) return unless team_belongs_to_account?(team_ids)
@account.teams.find_by(id: team_ids)
@conversation.update!(team_id: team_ids[0]) @conversation.update!(team_id: team_ids[0])
end end
def assign_best_agents(agent_ids = []) def assign_best_agent(agent_ids = [])
return unless agent_belongs_to_account?(agent_ids) return unless agent_belongs_to_account?(agent_ids)
@agent = @account.users.find_by(id: agent_ids) @agent = @account.users.find_by(id: agent_ids)
@conversation.update_assignee(@agent)
@conversation.update!(assignee_id: @agent.id) if @agent.present?
end end
def add_label(labels = []) def add_label(labels)
return if labels.empty?
@conversation.add_labels(labels) @conversation.add_labels(labels)
end end
@ -43,11 +71,11 @@ class AutomationRules::ActionService
case @rule.event_name case @rule.event_name
when 'conversation_created', 'conversation_status_changed' when 'conversation_created', 'conversation_status_changed'
TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message]) TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message])&.deliver_now
when 'conversation_updated' when 'conversation_updated'
TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message]) TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message])&.deliver_now
when 'message_created' when 'message_created'
TeamNotifications::AutomationNotificationMailer.message_created(@conversation, team, params[:message]) TeamNotifications::AutomationNotificationMailer.message_created(@conversation, team, params[:message])&.deliver_now
end end
end end

View file

@ -1,7 +1,7 @@
require 'json' require 'json'
class AutomationRules::ConditionsFilterService < FilterService class AutomationRules::ConditionsFilterService < FilterService
def initialize(rule, conversation) def initialize(rule, conversation = nil)
super([], nil) super([], nil)
@rule = rule @rule = rule
@conversation = conversation @conversation = conversation
@ -21,14 +21,14 @@ class AutomationRules::ConditionsFilterService < FilterService
records.any? records.any?
end end
def message_conditions(message) def message_conditions
message_filters = @filters['messages'] message_filters = @filters['messages']
@rule.conditions.each_with_index do |query_hash, current_index| @rule.conditions.each_with_index do |query_hash, current_index|
current_filter = message_filters[query_hash['attribute_key']] current_filter = message_filters[query_hash['attribute_key']]
@query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index) @query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index)
end end
records = Message.where(id: message.id).where(@query_string, @filter_values.with_indifferent_access) records = Message.where(conversation: @conversation).where(@query_string, @filter_values.with_indifferent_access)
records.any? records.any?
end end
@ -44,6 +44,19 @@ class AutomationRules::ConditionsFilterService < FilterService
end end
end end
# This will be used in future for contact automation rule
def contact_conditions(_contact)
conversation_filters = @filters['conversations']
@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)
end
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
records.any?
end
def conversation_query_string(current_filter, query_hash, current_index) def conversation_query_string(current_filter, query_hash, current_index)
attribute_key = query_hash['attribute_key'] attribute_key = query_hash['attribute_key']
query_operator = query_hash['query_operator'] query_operator = query_hash['query_operator']
@ -63,6 +76,6 @@ class AutomationRules::ConditionsFilterService < FilterService
end end
def base_relation def base_relation
Conversation.where(id: @conversation) Conversation.where(id: @conversation.id)
end end
end end

View file

@ -1,7 +1,7 @@
<p>Hi {{user.available_name}}</p> <p>Hi {{user.available_name}}</p>
<p>Time to save the world. A new conversation has been created in {{ inbox.name }}</p> <p>{{ custom_message }}</p>
<p> <p>
Click <a href="{{ action_url }}">here</a> to get cracking. Click <a href="{{ action_url }}">here</a> to get cracking.

View file

@ -0,0 +1,8 @@
<p>Hi {{user.available_name}}</p>
<p>{{ custom_message }}</p>
<p>
Click <a href="{{ action_url }}">here</a> to get cracking.
</p>

View file

@ -0,0 +1,8 @@
<p>Hi {{user.available_name}}</p>
<p>{{ custom_message }}</p>
<p>
Click <a href="{{ action_url }}">here</a> to get cracking.
</p>

View file

@ -2,12 +2,14 @@ module Current
thread_mattr_accessor :user thread_mattr_accessor :user
thread_mattr_accessor :account thread_mattr_accessor :account
thread_mattr_accessor :account_user thread_mattr_accessor :account_user
thread_mattr_accessor :executed_by
thread_mattr_accessor :contact thread_mattr_accessor :contact
def self.reset def self.reset
Current.user = nil Current.user = nil
Current.account = nil Current.account = nil
Current.account_user = nil Current.account_user = nil
Current.executed_by = nil
Current.contact = nil Current.contact = nil
end end
end end

View file

@ -31,16 +31,36 @@ describe AutomationRuleListener do
}, },
{ 'action_name' => 'assign_team', 'action_params' => [team.id] }, { 'action_name' => 'assign_team', 'action_params' => [team.id] },
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] }, { 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
{ 'action_name' => 'assign_best_agents', 'action_params' => [user_1.id] } { 'action_name' => 'send_webhook_events', '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' => 'mute_conversation', 'action_params' => nil },
{ 'action_name' => 'change_status', 'action_params' => ['snoozed'] },
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }
]) ])
end end
describe '#conversation_status_changed' do describe '#conversation_status_changed' do
context 'when rule matches' do context 'when rule matches' do
it 'triggers automation rule send webhook events' do
payload = conversation.webhook_data.merge(event: "automation_event: #{automation_rule.event_name}")
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
expect(WebhookJob).to receive(:perform_later).with('https://www.example.com', payload).once
listener.conversation_status_changed(event)
end
it 'triggers automation rule to assign team' do it 'triggers automation rule to assign team' do
expect(conversation.team_id).not_to eq(team.id) expect(conversation.team_id).not_to eq(team.id)
automation_rule automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
listener.conversation_status_changed(event) listener.conversation_status_changed(event)
conversation.reload conversation.reload
@ -51,6 +71,9 @@ describe AutomationRuleListener do
expect(conversation.labels).to eq([]) expect(conversation.labels).to eq([])
automation_rule automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
listener.conversation_status_changed(event) listener.conversation_status_changed(event)
conversation.reload conversation.reload
@ -61,12 +84,65 @@ describe AutomationRuleListener do
expect(conversation.assignee).to be_nil expect(conversation.assignee).to be_nil
automation_rule automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
listener.conversation_status_changed(event) listener.conversation_status_changed(event)
conversation.reload conversation.reload
expect(conversation.assignee).to eq(user_1) expect(conversation.assignee).to eq(user_1)
end end
it 'triggers automation rule send message to the contacts' do
expect(conversation.messages).to be_empty
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
listener.conversation_status_changed(event)
conversation.reload
expect(conversation.messages.last.content).to eq('Send this message.')
end
it 'triggers automation rule changes status to snoozed' do
expect(conversation.status).to eq('resolved')
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
listener.conversation_status_changed(event)
conversation.reload
expect(conversation.status).to eq('snoozed')
end
it 'triggers automation rule send email transcript to the mentioned email' do
mailer = double
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
listener.conversation_status_changed(event)
conversation.reload
allow(mailer).to receive(:conversation_transcript)
end
it 'triggers automation rule send email to the team' do
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
listener.conversation_status_changed(event)
end
end end
end end
@ -114,6 +190,42 @@ describe AutomationRuleListener do
expect(conversation.assignee).to eq(user_1) expect(conversation.assignee).to eq(user_1)
end end
it 'triggers automation rule send email transcript to the mentioned email' do
mailer = double
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated)
listener.conversation_updated(event)
conversation.reload
allow(mailer).to receive(:conversation_transcript)
end
it 'triggers automation rule send email to the team' do
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated)
listener.conversation_updated(event)
end
it 'triggers automation rule send message to the contacts' do
expect(conversation.messages).to be_empty
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated)
listener.conversation_updated(event)
conversation.reload
expect(conversation.messages.last.content).to eq('Send this message.')
end
end end
end end
@ -137,6 +249,9 @@ describe AutomationRuleListener do
expect(conversation.team_id).not_to eq(team.id) expect(conversation.team_id).not_to eq(team.id)
automation_rule automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created)
listener.message_created(event) listener.message_created(event)
conversation.reload conversation.reload
@ -147,22 +262,41 @@ describe AutomationRuleListener do
expect(conversation.labels).to eq([]) expect(conversation.labels).to eq([])
automation_rule automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created)
listener.message_created(event) listener.message_created(event)
conversation.reload conversation.reload
expect(conversation.labels.pluck(:name)).to eq(%w[support priority_customer]) expect(conversation.labels.pluck(:name)).to eq(%w[support priority_customer])
end end
it 'triggers automation rule to assign best agents' do it 'triggers automation rule to assign best agent' do
expect(conversation.assignee).to be_nil expect(conversation.assignee).to be_nil
automation_rule automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created)
listener.message_created(event) listener.message_created(event)
conversation.reload conversation.reload
expect(conversation.assignee).to eq(user_1) expect(conversation.assignee).to eq(user_1)
end end
it 'triggers automation rule send email transcript to the mentioned email' do
mailer = double
automation_rule
expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created)
listener.message_created(event)
conversation.reload
allow(mailer).to receive(:conversation_transcript)
end
end end
end end
end end

View file

@ -2,11 +2,13 @@ require 'rails_helper'
RSpec.describe AutomationRule, type: :model do RSpec.describe AutomationRule, type: :model do
describe 'associations' do describe 'associations' do
let(:account) { create(:account) }
let(:params) do let(:params) do
{ {
name: 'Notify Conversation Created and mark priority query', name: 'Notify Conversation Created and mark priority query',
description: 'Notify all administrator about conversation created and mark priority query', description: 'Notify all administrator about conversation created and mark priority query',
event_name: 'conversation_created', event_name: 'conversation_created',
account_id: account.id,
conditions: [ conditions: [
{ {
attribute_key: 'browser_language', attribute_key: 'browser_language',