diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index a30394edc..f84e38ca7 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -8,9 +8,11 @@ class Messages::MessageBuilder @conversation = conversation @user = user @message_type = params[:message_type] || 'outgoing' - @items = params.to_unsafe_h&.dig(:content_attributes, :items) @attachments = params[:attachments] + return unless params.instance_of?(ActionController::Parameters) + @in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to) + @items = params.to_unsafe_h&.dig(:content_attributes, :items) end def perform diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb new file mode 100644 index 000000000..0ec35c765 --- /dev/null +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -0,0 +1,21 @@ +class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController + before_action :check_authorization + + def index + @automation_rules = Current.account.automation_rules + end + + def create + @automation_rule = Current.account.automation_rules.create(automation_rules_permit) + end + + private + + def automation_rules_permit + params.permit( + :name, :description, :event_name, :account_id, + conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }], + actions: [:action_name, { action_params: [:intiated_at] }] + ) + end +end diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index 232dcff77..3f95405f9 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -16,7 +16,8 @@ class AsyncDispatcher < BaseDispatcher HookListener.instance, InstallationWebhookListener.instance, NotificationListener.instance, - WebhookListener.instance + WebhookListener.instance, + AutomationRuleListener.instance ] end end diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb new file mode 100644 index 000000000..9562e4ffe --- /dev/null +++ b/app/listeners/automation_rule_listener.rb @@ -0,0 +1,29 @@ +class AutomationRuleListener < BaseListener + def conversation_status_changed(event_obj) + conversation = event_obj.data[:conversation] + return unless rule_present?('conversation_status_changed', conversation) + + @rules.each do |rule| + conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform + AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present? + end + end + + def conversation_created(event_obj) + conversation = event_obj.data[:conversation] + return unless rule_present?('conversation_created', conversation) + + @rules.each do |rule| + conditions_match = AutomationRule::ConditionsFilterService.new(rule, conversation).perform + AutomationRule::ActionService.new(rule, conversation).perform if conditions_match.present? + end + end + + def rule_present?(event_name, conversation) + @rules = AutomationRule.where( + event_name: event_name, + account_id: conversation.account_id + ) + @rules.any? + end +end diff --git a/app/mailers/team_notifications/automation_notification_mailer.rb b/app/mailers/team_notifications/automation_notification_mailer.rb new file mode 100644 index 000000000..a6d219c54 --- /dev/null +++ b/app/mailers/team_notifications/automation_notification_mailer.rb @@ -0,0 +1,53 @@ +class TeamNotifications::AutomationNotificationMailer < ApplicationMailer + def conversation_creation(conversation, team, message) + return unless smtp_config_set_or_development? + + @agents = team.team_members + @conversation = conversation + @message = message + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + + send_an_email_to_team + end + + def conversation_updated(conversation, team) + return unless smtp_config_set_or_development? + + @agents = team.team_members + @conversation = conversation + @message = message + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + + send_an_email_to_team + end + + def message_created(message, agent) + return unless smtp_config_set_or_development? + + @agent = agent + @conversation = message.conversation + @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) + send_mail_with_liquid(to: @agent.email, subject: subject) + end + + private + + def send_an_email_to_team + @agents.each do |agent| + subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}." + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + send_mail_with_liquid(to: agent.email, subject: subject) + end + end + + def liquid_droppables + super.merge({ + user: @agent, + conversation: @conversation, + inbox: @conversation.inbox, + message: @message + }) + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 657517c8d..c905265a9 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -69,6 +69,7 @@ class Account < ApplicationRecord has_many :webhooks, dependent: :destroy_async has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp' has_many :working_hours, dependent: :destroy_async + has_many :automation_rules, dependent: :destroy has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING) diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb new file mode 100644 index 000000000..84ae6b51c --- /dev/null +++ b/app/models/automation_rule.rb @@ -0,0 +1,44 @@ +# == Schema Information +# +# Table name: automation_rules +# +# id :bigint not null, primary key +# actions :jsonb not null +# conditions :jsonb not null +# description :text +# event_name :string not null +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# +# Indexes +# +# index_automation_rules_on_account_id (account_id) +# +class AutomationRule < ApplicationRecord + belongs_to :account + + validates :account, presence: true + validate :json_conditions_format + validate :json_actions_format + + CONDITIONS_ATTRS = %w[country_code status browser_language assignee_id team_id referer].freeze + ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents].freeze + + private + + def json_conditions_format + return if conditions.nil? + + attributes = conditions.map { |obj, _| obj['attribute_key'] } + (attributes - CONDITIONS_ATTRS).blank? + end + + def json_actions_format + return if actions.nil? + + attributes = actions.map { |obj, _| obj['attribute_key'] } + (attributes - ACTIONS_ATTRS).blank? + end +end diff --git a/app/models/concerns/labelable.rb b/app/models/concerns/labelable.rb index 4e654fdd4..5a71fe84f 100644 --- a/app/models/concerns/labelable.rb +++ b/app/models/concerns/labelable.rb @@ -8,4 +8,9 @@ module Labelable def update_labels(labels = nil) update!(label_list: labels) end + + def add_labels(new_labels = nil) + new_labels << labels + update!(label_list: new_labels) + end end diff --git a/app/policies/automation_rule_policy.rb b/app/policies/automation_rule_policy.rb new file mode 100644 index 000000000..bd644f075 --- /dev/null +++ b/app/policies/automation_rule_policy.rb @@ -0,0 +1,9 @@ +class AutomationRulePolicy < ApplicationPolicy + def index? + @account_user.administrator? + end + + def create? + @account_user.administrator? + end +end diff --git a/app/services/automation_rules/action_service.rb b/app/services/automation_rules/action_service.rb new file mode 100644 index 000000000..28327959a --- /dev/null +++ b/app/services/automation_rules/action_service.rb @@ -0,0 +1,63 @@ +class AutomationRules::ActionService + def initialize(rule, conversation) + @rule = rule + @conversation = conversation + @account = @conversation.account + end + + def perform + @rule.actions.each do |action, _current_index| + action = action.with_indifferent_access + send(action[:action_name], action[:action_params]) + end + end + + private + + def send_message(message) + # params = { content: message, private: false } + # mb = Messages::MessageBuilder.new(@administrator, @conversation, params) + # mb.perform + end + + def assign_team(team_ids = []) + return unless team_belongs_to_account?(team_ids) + + @account.teams.find_by(id: team_ids) + @conversation.update!(team_id: team_ids[0]) + end + + def assign_best_agents(agent_ids = []) + return unless agent_belongs_to_account?(agent_ids) + + @agent = @account.users.find_by(id: agent_ids) + @conversation.update_assignee(@agent) + end + + def add_label(labels = []) + @conversation.add_labels(labels) + end + + def send_email_to_team(params) + team = Team.find(params[:team_ids][0]) + + case @rule.event_name + when 'conversation_created', 'conversation_status_changed' + TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message]) + when 'conversation_updated' + TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message]) + end + end + + def administrator + @administrator ||= @account.administrators.first + end + + def agent_belongs_to_account?(agent_ids) + @account.agents.pluck(:id).include?(agent_ids[0]) + end + + def team_belongs_to_account?(team_ids) + @account.team_ids.include?(team_ids[0]) + end +end diff --git a/app/services/automation_rules/conditions_filter_service.rb b/app/services/automation_rules/conditions_filter_service.rb new file mode 100644 index 000000000..bce3b581d --- /dev/null +++ b/app/services/automation_rules/conditions_filter_service.rb @@ -0,0 +1,45 @@ +require 'json' + +class AutomationRules::ConditionsFilterService < FilterService + def initialize(rule, conversation) + super([], nil) + @rule = rule + @conversation = conversation + file = File.read('./lib/filters/filter_keys.json') + @filters = JSON.parse(file) + end + + def perform + 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) + attribute_key = query_hash['attribute_key'] + query_operator = query_hash['query_operator'] + + filter_operator_value = filter_operation(query_hash, current_index) + + case current_filter['attribute_type'] + when 'additional_attributes' + " conversations.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} " + when 'standard' + if attribute_key == 'labels' + " tags.id #{filter_operator_value} #{query_operator} " + else + " conversations.#{attribute_key} #{filter_operator_value} #{query_operator} " + end + end + end + + def base_relation + Conversation.where(id: @conversation) + end +end diff --git a/app/services/instagram/send_on_instagram_service.rb b/app/services/instagram/send_on_instagram_service.rb index 0a8e9d4ec..8dfaf43ec 100644 --- a/app/services/instagram/send_on_instagram_service.rb +++ b/app/services/instagram/send_on_instagram_service.rb @@ -51,7 +51,6 @@ class Instagram::SendOnInstagramService < Base::SendOnChannelService def send_to_facebook_page(message_content) access_token = channel.page_access_token app_secret_proof = calculate_app_secret_proof(GlobalConfigService.load('FB_APP_SECRET', ''), access_token) - query = { access_token: access_token } query[:appsecret_proof] = app_secret_proof if app_secret_proof diff --git a/app/views/api/v1/accounts/automation_rules/create.json.jbuilder b/app/views/api/v1/accounts/automation_rules/create.json.jbuilder new file mode 100644 index 000000000..f2f893279 --- /dev/null +++ b/app/views/api/v1/accounts/automation_rules/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule diff --git a/app/views/api/v1/accounts/automation_rules/index.json.jbuilder b/app/views/api/v1/accounts/automation_rules/index.json.jbuilder new file mode 100644 index 000000000..266ab14b8 --- /dev/null +++ b/app/views/api/v1/accounts/automation_rules/index.json.jbuilder @@ -0,0 +1,5 @@ +json.data do + json.array! @automation_rules do |automation_rule| + json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: automation_rule + end +end diff --git a/app/views/api/v1/accounts/automation_rules/partials/_automation_rule.json.jbuilder b/app/views/api/v1/accounts/automation_rules/partials/_automation_rule.json.jbuilder new file mode 100644 index 000000000..9799a4339 --- /dev/null +++ b/app/views/api/v1/accounts/automation_rules/partials/_automation_rule.json.jbuilder @@ -0,0 +1,7 @@ +json.id automation_rule.id +json.account_id automation_rule.account_id +json.name automation_rule.name +json.description automation_rule.description +json.event_name automation_rule.event_name +json.conditions automation_rule.conditions +json.actions automation_rule.actions diff --git a/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid b/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid new file mode 100644 index 000000000..e324dd9e6 --- /dev/null +++ b/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid @@ -0,0 +1,8 @@ +

Hi {{user.available_name}}

+ + +

Time to save the world. A new conversation has been created in {{ inbox.name }}

+ +

+Click here to get cracking. +

diff --git a/config/routes.rb b/config/routes.rb index 47835f1bf..493810c03 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,6 +52,7 @@ Rails.application.routes.draw do end end resources :canned_responses, except: [:show, :edit, :new] + resources :automation_rules, only: [:create, :index] resources :campaigns, only: [:index, :create, :show, :update, :destroy] namespace :channels do diff --git a/db/migrate/20211110101046_create_automation_rules.rb b/db/migrate/20211110101046_create_automation_rules.rb new file mode 100644 index 000000000..ee7349413 --- /dev/null +++ b/db/migrate/20211110101046_create_automation_rules.rb @@ -0,0 +1,14 @@ +class CreateAutomationRules < ActiveRecord::Migration[6.1] + def change + create_table :automation_rules do |t| + t.bigint :account_id, null: false + t.string :name, null: false + t.text :description + t.string :event_name, null: false + t.jsonb :conditions, null: false, default: '{}' + t.jsonb :actions, null: false, default: '{}' + t.timestamps + t.index :account_id, name: 'index_automation_rules_on_account_id' + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1429612cd..14b64b78a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -124,6 +124,18 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do t.string "extension" end + create_table "automation_rules", force: :cascade do |t| + t.bigint "account_id", null: false + t.string "name", null: false + t.text "description" + t.string "event_name", null: false + t.jsonb "conditions", default: "{}", null: false + t.jsonb "actions", default: "{}", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["account_id"], name: "index_automation_rules_on_account_id" + end + create_table "campaigns", force: :cascade do |t| t.integer "display_id", null: false t.string "title", null: false diff --git a/lib/automation_rules/conditions.json b/lib/automation_rules/conditions.json new file mode 100644 index 000000000..ad96b5289 --- /dev/null +++ b/lib/automation_rules/conditions.json @@ -0,0 +1,160 @@ +{ + "conversations": { + "status": { + "attribute_name": "Status", + "input_type": "multi_select", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to" ], + "attribute_type": "standard" + }, + "assignee_id": { + "attribute_name": "Assignee Name", + "input_type": "search_box with name tags/plain text", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "contact_id": { + "attribute_name": "Contact Name", + "input_type": "plain_text", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "inbox_id": { + "attribute_name": "Inbox Name", + "input_type": "search_box", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "team_id": { + "attribute_name": "Team Name", + "input_type": "search_box", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "id": { + "attribute_name": "Conversation Identifier", + "input_type": "textbox", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "campaign_id": { + "attribute_name": "Campaign Name", + "input_type": "textbox", + "data_type": "Number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "labels": { + "attribute_name": "Labels", + "input_type": "tags", + "data_type": "text", + "filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ], + "attribute_type": "standard" + }, + "browser_language": { + "attribute_name": "Browser Language", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ], + "attribute_type": "additional_attributes" + }, + "country_code": { + "attribute_name": "Country Name", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + }, + "referer": { + "attribute_name": "Referer link", + "input_type": "textbox", + "data_type": "link", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + }, + "plan": { + "attribute_name": "Plan", + "input_type": "multi_select", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + } + }, + "contacts": { + "assignee_id": { + "attribute_name": "Assignee Name", + "input_type": "search_box with name tags/plain text", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "contact_id": { + "attribute_name": "Contact Name", + "input_type": "plain_text", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "inbox_id": { + "attribute_name": "Inbox Name", + "input_type": "search_box", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "team_id": { + "attribute_name": "Team Name", + "input_type": "search_box", + "data_type": "number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "id": { + "attribute_name": "Conversation Identifier", + "input_type": "textbox", + "data_type": "Number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "campaign_id": { + "attribute_name": "Campaign Name", + "input_type": "textbox", + "data_type": "Number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "labels": { + "attribute_name": "Labels", + "input_type": "tags", + "data_type": "text", + "filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ], + "attribute_type": "standard" + }, + "browser_language": { + "attribute_name": "Browser Language", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ], + "attribute_type": "additional_attributes" + }, + "country_code": { + "attribute_name": "Country Name", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + }, + "referer": { + "attribute_name": "Referer link", + "input_type": "textbox", + "data_type": "link", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + } + } +} diff --git a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb new file mode 100644 index 000000000..d922c7f06 --- /dev/null +++ b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb @@ -0,0 +1,118 @@ +require 'rails_helper' + +RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do + let(:account) { create(:account) } + let(:administrator) { create(:user, account: account, role: :administrator) } + let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) } + let!(:contact) { create(:contact, account: account) } + let(:contact_inbox) { create(:contact_inbox, inbox_id: inbox.id, contact_id: contact.id) } + + describe 'GET /api/v1/accounts/{account.id}/automation_rules' do + context 'when it is an authenticated user' do + it 'returns all records' do + automation_rule = create(:automation_rule, account: account, name: 'Test Automation Rule') + + get "/api/v1/accounts/#{account.id}/automation_rules", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body[:data].first[:id]).to eq(automation_rule.id) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/automation_rules" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST /api/v1/accounts/{account.id}/automation_rules' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/automation_rules" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:params) do + { + name: 'Notify Conversation Created and mark priority query', + description: 'Notify all administrator about conversation created and mark priority query', + event_name: 'conversation_created', + conditions: [ + { + attribute_key: 'browser_language', + filter_operator: 'equal_to', + values: ['en'], + query_operator: 'AND' + }, + { + attribute_key: 'country_code', + filter_operator: 'equal_to', + values: %w[USA UK], + query_operator: nil + } + ], + actions: [ + { + action_name: :send_message, + action_params: ['Welcome to the chatwoot platform.'] + }, + { + action_name: :assign_team, + action_params: [1] + }, + { + action_name: :add_label, + action_params: %w[support priority_customer] + }, + { + action_name: :assign_best_administrator, + action_params: [1] + }, + { + action_name: :update_additional_attributes, + action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }] + } + ] + }.with_indifferent_access + end + + it 'Saves for automation_rules for account with country_code and browser_language conditions' do + expect(account.automation_rules.count).to eq(0) + + post "/api/v1/accounts/#{account.id}/automation_rules", + headers: administrator.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + expect(account.automation_rules.count).to eq(1) + end + + it 'Saves for automation_rules for account with status conditions' do + params[:conditions] = [ + { + attribute_key: 'status', + filter_operator: 'equal_to', + values: ['resolved'], + query_operator: nil + }.with_indifferent_access + ] + expect(account.automation_rules.count).to eq(0) + + post "/api/v1/accounts/#{account.id}/automation_rules", + headers: administrator.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + expect(account.automation_rules.count).to eq(1) + end + end + end +end diff --git a/spec/factories/automation_rules.rb b/spec/factories/automation_rules.rb new file mode 100644 index 000000000..1b37e40d2 --- /dev/null +++ b/spec/factories/automation_rules.rb @@ -0,0 +1,19 @@ +FactoryBot.define do + factory :automation_rule do + account + event_name { 'conversation_status_changed' } + conditions { [{ 'values': ['resolved'], 'attribute_key': 'status', 'query_operator': nil, 'filter_operator': 'equal_to' }] } + actions do + [ + { + 'action_name' => 'send_email_to_team', 'action_params' => { + 'message' => 'Please pay attention to this conversation, its from high priority customer', 'team_ids' => [1] + } + }, + { 'action_name' => 'assign_team', 'action_params' => [1] }, + { 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] }, + { 'action_name' => 'assign_best_agents', 'action_params' => [1, 2, 3, 4] } + ] + end + end +end diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb new file mode 100644 index 000000000..f14167f5f --- /dev/null +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' +describe AutomationRuleListener do + let(:listener) { described_class.instance } + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:contact) { create(:contact, account: account, identifier: '123') } + let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) } + let(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: inbox, account: account) } + let(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') } + 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(:team_member, user: user_1, team: team) + create(:team_member, user: user_2, team: team) + create(:account_user, user: user_2, account: account) + create(:account_user, user: user_1, account: account) + + conversation.resolved! + automation_rule.update!(actions: + [ + { + 'action_name' => 'send_email_to_team', 'action_params' => { + 'message' => 'Please pay attention to this conversation, its from high priority customer', + 'team_ids' => [team.id] + } + }, + { 'action_name' => 'assign_team', 'action_params' => [team.id] }, + { 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] }, + { 'action_name' => 'assign_best_agents', 'action_params' => [user_1.id] } + ]) + end + + describe '#conversation_status_changed' do + context 'when rule matches' do + it 'triggers automation rule to assign team' do + expect(conversation.team_id).not_to eq(team.id) + + automation_rule + listener.conversation_status_changed(event) + + conversation.reload + expect(conversation.team_id).to eq(team.id) + end + + it 'triggers automation rule to add label' do + expect(conversation.labels).to eq([]) + + automation_rule + listener.conversation_status_changed(event) + + conversation.reload + expect(conversation.labels.pluck(:name)).to eq(%w[support priority_customer]) + end + + it 'triggers automation rule to assign best agents' do + expect(conversation.assignee).to be_nil + + automation_rule + listener.conversation_status_changed(event) + + conversation.reload + + expect(conversation.assignee).to eq(user_1) + end + end + end +end diff --git a/spec/models/automation_rule_spec.rb b/spec/models/automation_rule_spec.rb new file mode 100644 index 000000000..6b06c3197 --- /dev/null +++ b/spec/models/automation_rule_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +RSpec.describe AutomationRule, type: :model do + describe 'associations' do + let(:params) do + { + name: 'Notify Conversation Created and mark priority query', + description: 'Notify all administrator about conversation created and mark priority query', + event_name: 'conversation_created', + conditions: [ + { + attribute_key: 'browser_language', + filter_operator: 'equal_to', + values: ['en'], + query_operator: 'AND' + }, + { + attribute_key: 'country_code', + filter_operator: 'equal_to', + values: %w[USA UK], + query_operator: nil + } + ], + actions: [ + { + action_name: :send_message, + action_params: ['Welcome to the chatwoot platform.'] + }, + { + action_name: :assign_team, + action_params: [1] + }, + { + action_name: :add_label, + action_params: %w[support priority_customer] + }, + { + action_name: :assign_best_administrator, + action_params: [1] + }, + { + action_name: :update_additional_attributes, + action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }] + } + ] + }.with_indifferent_access + end + + it 'returns valid record' do + rule = FactoryBot.build(:automation_rule, params) + expect(rule.valid?).to eq true + end + end +end