From 1ca1b4d36b014bdc918c6adbbb69cda16b46c106 Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Wed, 23 Feb 2022 16:53:36 +0530 Subject: [PATCH] feat: bulk actions to update conversation objects (#3934) Added the endpoints for bulk updating conversation objects Fixes: #3845 #3940 #3943 --- .../v1/accounts/bulk_actions_controller.rb | 26 ++++ app/jobs/bulk_actions_job.rb | 59 +++++++ config/routes.rb | 1 + .../accounts/bulk_actions_controller_spec.rb | 145 ++++++++++++++++++ spec/jobs/bulk_actions_job_spec.rb | 63 ++++++++ spec/services/contacts/filter_service_spec.rb | 2 +- 6 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/accounts/bulk_actions_controller.rb create mode 100644 app/jobs/bulk_actions_job.rb create mode 100644 spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb create mode 100644 spec/jobs/bulk_actions_job_spec.rb diff --git a/app/controllers/api/v1/accounts/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/bulk_actions_controller.rb new file mode 100644 index 000000000..832f5a49b --- /dev/null +++ b/app/controllers/api/v1/accounts/bulk_actions_controller.rb @@ -0,0 +1,26 @@ +class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController + before_action :type_matches? + + def create + if type_matches? + ::BulkActionsJob.perform_later( + account: @current_account, + user: current_user, + params: permitted_params + ) + head :ok + else + render json: { success: false }, status: :unprocessable_entity + end + end + + private + + def type_matches? + ['Conversation'].include?(params[:type]) + end + + def permitted_params + params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) + end +end diff --git a/app/jobs/bulk_actions_job.rb b/app/jobs/bulk_actions_job.rb new file mode 100644 index 000000000..75b834308 --- /dev/null +++ b/app/jobs/bulk_actions_job.rb @@ -0,0 +1,59 @@ +class BulkActionsJob < ApplicationJob + queue_as :medium + attr_accessor :records + + MODEL_TYPE = ['Conversation'].freeze + + def perform(account:, params:, user:) + @account = account + Current.user = user + @params = params + @records = records_to_updated(params[:ids]) + bulk_update + ensure + Current.reset + end + + def bulk_update + bulk_remove_labels + bulk_conversation_update + end + + def bulk_conversation_update + params = available_params(@params) + records.each do |conversation| + bulk_add_labels(conversation) + conversation.update(params) if params + end + end + + def bulk_remove_labels + records.each do |conversation| + remove_labels(conversation) + end + end + + def available_params(params) + return unless params[:fields] + + params[:fields].delete_if { |_k, v| v.nil? } + end + + def bulk_add_labels(conversation) + conversation.add_labels(@params[:labels][:add]) if @params[:labels] && @params[:labels][:add] + end + + def remove_labels(conversation) + return unless @params[:labels] && @params[:labels][:remove] + + labels = conversation.label_list - @params[:labels][:remove] + conversation.update(label_list: labels) + end + + def records_to_updated(ids) + current_model = @params[:type].camelcase + return unless MODEL_TYPE.include?(current_model) + + current_model.constantize&.where(account_id: @account.id, display_id: ids) + end +end diff --git a/config/routes.rb b/config/routes.rb index f90b6b84b..b450e02fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,6 +40,7 @@ Rails.application.routes.draw do resource :contact_merge, only: [:create] end + resource :bulk_actions, only: [:create] resources :agents, only: [:index, :create, :update, :destroy] resources :agent_bots, only: [:index, :create, :show, :update, :destroy] diff --git a/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb b/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb new file mode 100644 index 000000000..598b2789c --- /dev/null +++ b/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb @@ -0,0 +1,145 @@ +require 'rails_helper' + +RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do + include ActiveJob::TestHelper + let(:account) { create(:account) } + let(:agent_1) { create(:user, account: account, role: :agent) } + let(:agent_2) { create(:user, account: account, role: :agent) } + + before do + create(:conversation, account_id: account.id, status: :open) + create(:conversation, account_id: account.id, status: :open) + create(:conversation, account_id: account.id, status: :open) + create(:conversation, account_id: account.id, status: :open) + end + + describe 'POST /api/v1/accounts/{account.id}/bulk_action' do + context 'when it is an unauthenticated user' do + let(:agent) { create(:user) } + + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: { type: 'Conversation', fields: { status: 'open' }, ids: [1, 2, 3] } + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'Ignores bulk_actions for wrong type' do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: { type: 'Test', fields: { status: 'snoozed' }, ids: %w[1 2 3] } + + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'Bulk update conversation status' do + expect(Conversation.first.status).to eq('open') + expect(Conversation.last.status).to eq('open') + expect(Conversation.first.assignee_id).to eq(nil) + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: { type: 'Conversation', fields: { status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) } + + expect(response).to have_http_status(:success) + end + + expect(Conversation.first.status).to eq('snoozed') + expect(Conversation.last.status).to eq('open') + expect(Conversation.first.assignee_id).to eq(nil) + end + + it 'Bulk update conversation assignee id' do + params = { type: 'Conversation', fields: { assignee_id: agent_1.id }, ids: Conversation.first(3).pluck(:display_id) } + + expect(Conversation.first.status).to eq('open') + expect(Conversation.first.assignee_id).to eq(nil) + expect(Conversation.second.assignee_id).to eq(nil) + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + end + + expect(Conversation.first.assignee_id).to eq(agent_1.id) + expect(Conversation.second.assignee_id).to eq(agent_1.id) + expect(Conversation.first.status).to eq('open') + end + + it 'Bulk update conversation status and assignee id' do + params = { type: 'Conversation', fields: { assignee_id: agent_1.id, status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) } + + expect(Conversation.first.status).to eq('open') + expect(Conversation.second.assignee_id).to eq(nil) + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + end + + expect(Conversation.first.assignee_id).to eq(agent_1.id) + expect(Conversation.second.assignee_id).to eq(agent_1.id) + expect(Conversation.first.status).to eq('snoozed') + expect(Conversation.second.status).to eq('snoozed') + end + + it 'Bulk update conversation labels' do + params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { add: %w[support priority_customer] } } + + expect(Conversation.first.labels).to eq([]) + expect(Conversation.second.labels).to eq([]) + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + end + + expect(Conversation.first.label_list).to eq(%w[support priority_customer]) + expect(Conversation.second.label_list).to eq(%w[support priority_customer]) + end + end + end + + describe 'POST /api/v1/accounts/{account.id}/bulk_actions' do + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'Bulk delete conversation labels' do + Conversation.first.add_labels(%w[support priority_customer]) + Conversation.second.add_labels(%w[support priority_customer]) + Conversation.third.add_labels(%w[support priority_customer]) + + params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { remove: %w[support] } } + + expect(Conversation.first.label_list).to eq(%w[support priority_customer]) + expect(Conversation.second.label_list).to eq(%w[support priority_customer]) + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + end + + expect(Conversation.first.label_list).to eq(['priority_customer']) + expect(Conversation.second.label_list).to eq(['priority_customer']) + end + end + end +end diff --git a/spec/jobs/bulk_actions_job_spec.rb b/spec/jobs/bulk_actions_job_spec.rb new file mode 100644 index 000000000..028f5ff3d --- /dev/null +++ b/spec/jobs/bulk_actions_job_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +RSpec.describe BulkActionsJob, type: :job do + params = { + type: 'Conversation', + fields: { status: 'snoozed' }, + ids: Conversation.first(3).pluck(:display_id) + } + + subject(:job) { described_class.perform_later(account: account, params: params, user: agent) } + + let(:account) { create(:account) } + let!(:agent) { create(:user, account: account, role: :agent) } + let!(:conversation_1) { create(:conversation, account_id: account.id, status: :open) } + let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) } + let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) } + + it 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .with(account: account, params: params, user: agent) + .on_queue('medium') + end + + context 'when job is triggered' do + let(:bulk_action_job) { double } + + before do + allow(bulk_action_job).to receive(:perform) + end + + it 'bulk updates the status' do + params = { + type: 'Conversation', + fields: { status: 'snoozed', assignee_id: agent.id }, + ids: Conversation.first(3).pluck(:display_id) + } + + expect(Conversation.first.status).to eq('open') + + described_class.perform_now(account: account, params: params, user: agent) + + expect(conversation_1.reload.status).to eq('snoozed') + expect(conversation_2.reload.status).to eq('snoozed') + expect(conversation_3.reload.status).to eq('snoozed') + end + + it 'bulk updates the assignee_id' do + params = { + type: 'Conversation', + fields: { status: 'snoozed', assignee_id: agent.id }, + ids: Conversation.first(3).pluck(:display_id) + } + + expect(Conversation.first.assignee_id).to eq(nil) + + described_class.perform_now(account: account, params: params, user: agent) + + expect(Conversation.first.assignee_id).to eq(agent.id) + expect(Conversation.second.assignee_id).to eq(agent.id) + expect(Conversation.third.assignee_id).to eq(agent.id) + end + end +end diff --git a/spec/services/contacts/filter_service_spec.rb b/spec/services/contacts/filter_service_spec.rb index e4a05946a..5f52a1a3c 100644 --- a/spec/services/contacts/filter_service_spec.rb +++ b/spec/services/contacts/filter_service_spec.rb @@ -162,7 +162,7 @@ describe ::Contacts::FilterService do expected_count = Contact.where("created_at < ? AND custom_attributes->>'customer_type' = ?", Date.tomorrow, 'platinum').count expect(result[:contacts].length).to be expected_count - expect(result[:contacts].first.id).to eq(el_contact.id) + expect(result[:contacts].pluck(:id)).to include(el_contact.id) end context 'with x_days_before filter' do