diff --git a/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb b/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb new file mode 100644 index 000000000..ac1d0a712 --- /dev/null +++ b/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb @@ -0,0 +1,20 @@ +class Api::V1::Accounts::AssignmentPolicies::InboxesController < Api::V1::Accounts::BaseController + before_action :fetch_assignment_policy + before_action -> { check_authorization(AssignmentPolicy) } + + def index + @inboxes = @assignment_policy.inboxes + end + + private + + def fetch_assignment_policy + @assignment_policy = Current.account.assignment_policies.find( + params[:assignment_policy_id] + ) + end + + def permitted_params + params.permit(:assignment_policy_id) + end +end diff --git a/app/controllers/api/v1/accounts/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/assignment_policies_controller.rb new file mode 100644 index 000000000..1807d6afb --- /dev/null +++ b/app/controllers/api/v1/accounts/assignment_policies_controller.rb @@ -0,0 +1,36 @@ +class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseController + before_action :fetch_assignment_policy, only: [:show, :update, :destroy] + before_action :check_authorization + + def index + @assignment_policies = Current.account.assignment_policies + end + + def show; end + + def create + @assignment_policy = Current.account.assignment_policies.create!(assignment_policy_params) + end + + def update + @assignment_policy.update!(assignment_policy_params) + end + + def destroy + @assignment_policy.destroy! + head :ok + end + + private + + def fetch_assignment_policy + @assignment_policy = Current.account.assignment_policies.find(params[:id]) + end + + def assignment_policy_params + params.require(:assignment_policy).permit( + :name, :description, :assignment_order, :conversation_priority, + :fair_distribution_limit, :fair_distribution_window, :enabled + ) + end +end diff --git a/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb new file mode 100644 index 000000000..cf52951a5 --- /dev/null +++ b/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb @@ -0,0 +1,46 @@ +class Api::V1::Accounts::Inboxes::AssignmentPoliciesController < Api::V1::Accounts::BaseController + before_action :fetch_inbox + before_action :fetch_assignment_policy, only: [:create] + before_action -> { check_authorization(AssignmentPolicy) } + before_action :validate_assignment_policy, only: [:show, :destroy] + + def show + @assignment_policy = @inbox.assignment_policy + end + + def create + # There should be only one assignment policy for an inbox. + # If there is a new request to add an assignment policy, we will + # delete the old one and attach the new policy + remove_inbox_assignment_policy + @inbox_assignment_policy = @inbox.create_inbox_assignment_policy!(assignment_policy: @assignment_policy) + @assignment_policy = @inbox.assignment_policy + end + + def destroy + remove_inbox_assignment_policy + head :ok + end + + private + + def remove_inbox_assignment_policy + @inbox.inbox_assignment_policy&.destroy + end + + def fetch_inbox + @inbox = Current.account.inboxes.find(permitted_params[:inbox_id]) + end + + def fetch_assignment_policy + @assignment_policy = Current.account.assignment_policies.find(permitted_params[:assignment_policy_id]) + end + + def permitted_params + params.permit(:assignment_policy_id, :inbox_id) + end + + def validate_assignment_policy + return render_not_found_error(I18n.t('errors.assignment_policy.not_found')) unless @inbox.assignment_policy + end +end diff --git a/app/models/account.rb b/app/models/account.rb index f8eb998f0..b84d7f526 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -61,6 +61,7 @@ class Account < ApplicationRecord has_many :agent_bots, dependent: :destroy_async has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api' has_many :articles, dependent: :destroy_async, class_name: '::Article' + has_many :assignment_policies, dependent: :destroy_async has_many :automation_rules, dependent: :destroy_async has_many :macros, dependent: :destroy_async has_many :campaigns, dependent: :destroy_async diff --git a/app/models/assignment_policy.rb b/app/models/assignment_policy.rb new file mode 100644 index 000000000..c01ab91c4 --- /dev/null +++ b/app/models/assignment_policy.rb @@ -0,0 +1,37 @@ +# == Schema Information +# +# Table name: assignment_policies +# +# id :bigint not null, primary key +# assignment_order :integer default(0), not null +# conversation_priority :integer default("earliest_created"), not null +# description :text +# enabled :boolean default(TRUE), not null +# fair_distribution_limit :integer default(100), not null +# fair_distribution_window :integer default(3600), not null +# name :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# +# Indexes +# +# index_assignment_policies_on_account_id (account_id) +# index_assignment_policies_on_account_id_and_name (account_id,name) UNIQUE +# index_assignment_policies_on_enabled (enabled) +# +class AssignmentPolicy < ApplicationRecord + belongs_to :account + has_many :inbox_assignment_policies, dependent: :destroy + has_many :inboxes, through: :inbox_assignment_policies + + validates :name, presence: true, uniqueness: { scope: :account_id } + validates :fair_distribution_limit, numericality: { greater_than: 0 } + validates :fair_distribution_window, numericality: { greater_than: 0 } + + enum conversation_priority: { earliest_created: 0, longest_waiting: 1 } + + enum assignment_order: { round_robin: 0 } unless ChatwootApp.enterprise? +end + +AssignmentPolicy.include_mod_with('Concerns::AssignmentPolicy') diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 1c898ba7f..27f096bfa 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -67,6 +67,8 @@ class Inbox < ApplicationRecord has_many :conversations, dependent: :destroy_async has_many :messages, dependent: :destroy_async + has_one :inbox_assignment_policy, dependent: :destroy + has_one :assignment_policy, through: :inbox_assignment_policy has_one :agent_bot_inbox, dependent: :destroy_async has_one :agent_bot, through: :agent_bot_inbox has_many :webhooks, dependent: :destroy_async diff --git a/app/models/inbox_assignment_policy.rb b/app/models/inbox_assignment_policy.rb new file mode 100644 index 000000000..c263ab40e --- /dev/null +++ b/app/models/inbox_assignment_policy.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: inbox_assignment_policies +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# assignment_policy_id :bigint not null +# inbox_id :bigint not null +# +# Indexes +# +# index_inbox_assignment_policies_on_assignment_policy_id (assignment_policy_id) +# index_inbox_assignment_policies_on_inbox_id (inbox_id) UNIQUE +# +class InboxAssignmentPolicy < ApplicationRecord + belongs_to :inbox + belongs_to :assignment_policy + + validates :inbox_id, uniqueness: true +end diff --git a/app/policies/assignment_policy_policy.rb b/app/policies/assignment_policy_policy.rb new file mode 100644 index 000000000..fcd0ee9bc --- /dev/null +++ b/app/policies/assignment_policy_policy.rb @@ -0,0 +1,21 @@ +class AssignmentPolicyPolicy < ApplicationPolicy + def index? + @account_user.administrator? + end + + def show? + @account_user.administrator? + end + + def create? + @account_user.administrator? + end + + def update? + @account_user.administrator? + end + + def destroy? + @account_user.administrator? + end +end diff --git a/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder new file mode 100644 index 000000000..b48307a94 --- /dev/null +++ b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder @@ -0,0 +1,10 @@ +json.id assignment_policy.id +json.name assignment_policy.name +json.description assignment_policy.description +json.assignment_order assignment_policy.assignment_order +json.conversation_priority assignment_policy.conversation_priority +json.fair_distribution_limit assignment_policy.fair_distribution_limit +json.fair_distribution_window assignment_policy.fair_distribution_window +json.enabled assignment_policy.enabled +json.created_at assignment_policy.created_at.to_i +json.updated_at assignment_policy.updated_at.to_i diff --git a/app/views/api/v1/accounts/assignment_policies/create.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/create.json.jbuilder new file mode 100644 index 000000000..8fd9543c3 --- /dev/null +++ b/app/views/api/v1/accounts/assignment_policies/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'assignment_policy', assignment_policy: @assignment_policy diff --git a/app/views/api/v1/accounts/assignment_policies/inboxes/create.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/inboxes/create.json.jbuilder new file mode 100644 index 000000000..c5aede050 --- /dev/null +++ b/app/views/api/v1/accounts/assignment_policies/inboxes/create.json.jbuilder @@ -0,0 +1,5 @@ +json.id @inbox_assignment_policy.id +json.inbox_id @inbox_assignment_policy.inbox_id +json.assignment_policy_id @inbox_assignment_policy.assignment_policy_id +json.created_at @inbox_assignment_policy.created_at.to_i +json.updated_at @inbox_assignment_policy.updated_at.to_i diff --git a/app/views/api/v1/accounts/assignment_policies/inboxes/index.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/inboxes/index.json.jbuilder new file mode 100644 index 000000000..5a22aa917 --- /dev/null +++ b/app/views/api/v1/accounts/assignment_policies/inboxes/index.json.jbuilder @@ -0,0 +1,3 @@ +json.inboxes @inboxes do |inbox| + json.partial! 'api/v1/models/inbox', formats: [:json], resource: inbox +end diff --git a/app/views/api/v1/accounts/assignment_policies/index.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/index.json.jbuilder new file mode 100644 index 000000000..0be431f87 --- /dev/null +++ b/app/views/api/v1/accounts/assignment_policies/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @assignment_policies do |assignment_policy| + json.partial! 'assignment_policy', assignment_policy: assignment_policy +end diff --git a/app/views/api/v1/accounts/assignment_policies/show.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/show.json.jbuilder new file mode 100644 index 000000000..8fd9543c3 --- /dev/null +++ b/app/views/api/v1/accounts/assignment_policies/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'assignment_policy', assignment_policy: @assignment_policy diff --git a/app/views/api/v1/accounts/assignment_policies/update.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/update.json.jbuilder new file mode 100644 index 000000000..8fd9543c3 --- /dev/null +++ b/app/views/api/v1/accounts/assignment_policies/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'assignment_policy', assignment_policy: @assignment_policy diff --git a/app/views/api/v1/accounts/inboxes/assignment_policies/create.json.jbuilder b/app/views/api/v1/accounts/inboxes/assignment_policies/create.json.jbuilder new file mode 100644 index 000000000..105658704 --- /dev/null +++ b/app/views/api/v1/accounts/inboxes/assignment_policies/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/accounts/assignment_policies/assignment_policy', formats: [:json], assignment_policy: @assignment_policy diff --git a/app/views/api/v1/accounts/inboxes/assignment_policies/show.json.jbuilder b/app/views/api/v1/accounts/inboxes/assignment_policies/show.json.jbuilder new file mode 100644 index 000000000..105658704 --- /dev/null +++ b/app/views/api/v1/accounts/inboxes/assignment_policies/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/accounts/assignment_policies/assignment_policy', formats: [:json], assignment_policy: @assignment_policy diff --git a/config/features.yml b/config/features.yml index db2d46700..d6f2d24a3 100644 --- a/config/features.yml +++ b/config/features.yml @@ -191,3 +191,7 @@ display_name: CRM V2 enabled: false chatwoot_internal: true +- name: assignment_v2 + display_name: Assignment V2 + enabled: false + chatwoot_internal: true diff --git a/config/locales/en.yml b/config/locales/en.yml index 1d8347679..e55132709 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -53,6 +53,8 @@ en: email_already_exists: 'You have already signed up for an account with %{email}' invalid_params: 'Invalid, please check the signup paramters and try again' failed: Signup failed + assignment_policy: + not_found: Assignment policy not found data_import: data_type: invalid: Invalid data type diff --git a/config/routes.rb b/config/routes.rb index 10749062e..1409b11fd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -217,6 +217,15 @@ Rails.application.routes.draw do end end + # Assignment V2 Routes + resources :assignment_policies do + resources :inboxes, only: [:index, :create, :destroy], module: :assignment_policies + end + + resources :inboxes, only: [] do + resource :assignment_policy, only: [:show, :create, :destroy], module: :inboxes + end + namespace :twitter do resource :authorization, only: [:create] end diff --git a/enterprise/app/models/enterprise/concerns/assignment_policy.rb b/enterprise/app/models/enterprise/concerns/assignment_policy.rb new file mode 100644 index 000000000..bddcc5e75 --- /dev/null +++ b/enterprise/app/models/enterprise/concerns/assignment_policy.rb @@ -0,0 +1,7 @@ +module Enterprise::Concerns::AssignmentPolicy + extend ActiveSupport::Concern + + included do + enum assignment_order: { round_robin: 0, balanced: 1 } if ChatwootApp.enterprise? + end +end diff --git a/spec/controllers/api/v1/accounts/assignment_policies/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/assignment_policies/inboxes_controller_spec.rb new file mode 100644 index 000000000..6b3c677c5 --- /dev/null +++ b/spec/controllers/api/v1/accounts/assignment_policies/inboxes_controller_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +RSpec.describe 'Assignment Policy Inboxes API', type: :request do + let(:account) { create(:account) } + let(:assignment_policy) { create(:assignment_policy, account: account) } + + describe 'GET /api/v1/accounts/{account_id}/assignment_policies/{assignment_policy_id}/inboxes' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + context 'when assignment policy has associated inboxes' do + before do + inbox1 = create(:inbox, account: account) + inbox2 = create(:inbox, account: account) + create(:inbox_assignment_policy, inbox: inbox1, assignment_policy: assignment_policy) + create(:inbox_assignment_policy, inbox: inbox2, assignment_policy: assignment_policy) + end + + it 'returns all inboxes associated with the assignment policy' do + get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['inboxes']).to be_an(Array) + expect(json_response['inboxes'].length).to eq(2) + end + end + + context 'when assignment policy has no associated inboxes' do + it 'returns empty array' do + get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['inboxes']).to eq([]) + end + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/controllers/api/v1/accounts/assignment_policies_controller_spec.rb b/spec/controllers/api/v1/accounts/assignment_policies_controller_spec.rb new file mode 100644 index 000000000..f882ec992 --- /dev/null +++ b/spec/controllers/api/v1/accounts/assignment_policies_controller_spec.rb @@ -0,0 +1,326 @@ +require 'rails_helper' + +RSpec.describe 'Assignment Policies API', type: :request do + let(:account) { create(:account) } + + describe 'GET /api/v1/accounts/{account.id}/assignment_policies' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/assignment_policies" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + before do + create_list(:assignment_policy, 3, account: account) + end + + it 'returns all assignment policies for the account' do + get "/api/v1/accounts/#{account.id}/assignment_policies", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response.length).to eq(3) + expect(json_response.first.keys).to include('id', 'name', 'description') + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/assignment_policies", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'GET /api/v1/accounts/{account.id}/assignment_policies/:id' do + let(:assignment_policy) { create(:assignment_policy, account: account) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + it 'returns the assignment policy' do + get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['id']).to eq(assignment_policy.id) + expect(json_response['name']).to eq(assignment_policy.name) + end + + it 'returns not found for non-existent policy' do + get "/api/v1/accounts/#{account.id}/assignment_policies/999999", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST /api/v1/accounts/{account.id}/assignment_policies' do + let(:valid_params) do + { + assignment_policy: { + name: 'New Assignment Policy', + description: 'Policy for new team', + conversation_priority: 'longest_waiting', + fair_distribution_limit: 15, + enabled: true + } + } + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/assignment_policies", params: valid_params + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + it 'creates a new assignment policy' do + expect do + post "/api/v1/accounts/#{account.id}/assignment_policies", + headers: admin.create_new_auth_token, + params: valid_params, + as: :json + end.to change(AssignmentPolicy, :count).by(1) + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['name']).to eq('New Assignment Policy') + expect(json_response['conversation_priority']).to eq('longest_waiting') + end + + it 'creates policy with minimal required params' do + minimal_params = { assignment_policy: { name: 'Minimal Policy' } } + + expect do + post "/api/v1/accounts/#{account.id}/assignment_policies", + headers: admin.create_new_auth_token, + params: minimal_params, + as: :json + end.to change(AssignmentPolicy, :count).by(1) + + expect(response).to have_http_status(:success) + end + + it 'prevents duplicate policy names within account' do + create(:assignment_policy, account: account, name: 'Duplicate Policy') + duplicate_params = { assignment_policy: { name: 'Duplicate Policy' } } + + expect do + post "/api/v1/accounts/#{account.id}/assignment_policies", + headers: admin.create_new_auth_token, + params: duplicate_params, + as: :json + end.not_to change(AssignmentPolicy, :count) + + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'validates required fields' do + invalid_params = { assignment_policy: { name: '' } } + + post "/api/v1/accounts/#{account.id}/assignment_policies", + headers: admin.create_new_auth_token, + params: invalid_params, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/assignment_policies", + headers: agent.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'PUT /api/v1/accounts/{account.id}/assignment_policies/:id' do + let(:assignment_policy) { create(:assignment_policy, account: account, name: 'Original Policy') } + let(:update_params) do + { + assignment_policy: { + name: 'Updated Policy', + description: 'Updated description', + fair_distribution_limit: 20 + } + } + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + params: update_params + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + it 'updates the assignment policy' do + put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: admin.create_new_auth_token, + params: update_params, + as: :json + + expect(response).to have_http_status(:success) + assignment_policy.reload + expect(assignment_policy.name).to eq('Updated Policy') + expect(assignment_policy.fair_distribution_limit).to eq(20) + end + + it 'allows partial updates' do + partial_params = { assignment_policy: { enabled: false } } + + put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: admin.create_new_auth_token, + params: partial_params, + as: :json + + expect(response).to have_http_status(:success) + expect(assignment_policy.reload.enabled).to be(false) + expect(assignment_policy.name).to eq('Original Policy') # unchanged + end + + it 'prevents duplicate names during update' do + create(:assignment_policy, account: account, name: 'Existing Policy') + duplicate_params = { assignment_policy: { name: 'Existing Policy' } } + + put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: admin.create_new_auth_token, + params: duplicate_params, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'returns not found for non-existent policy' do + put "/api/v1/accounts/#{account.id}/assignment_policies/999999", + headers: admin.create_new_auth_token, + params: update_params, + as: :json + + expect(response).to have_http_status(:not_found) + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: agent.create_new_auth_token, + params: update_params, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE /api/v1/accounts/{account.id}/assignment_policies/:id' do + let(:assignment_policy) { create(:assignment_policy, account: account) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + it 'deletes the assignment policy' do + assignment_policy # create it first + + expect do + delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: admin.create_new_auth_token, + as: :json + end.to change(AssignmentPolicy, :count).by(-1) + + expect(response).to have_http_status(:ok) + end + + it 'cascades deletion to associated inbox assignment policies' do + inbox = create(:inbox, account: account) + create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) + + expect do + delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: admin.create_new_auth_token, + as: :json + end.to change(InboxAssignmentPolicy, :count).by(-1) + + expect(response).to have_http_status(:ok) + end + + it 'returns not found for non-existent policy' do + delete "/api/v1/accounts/#{account.id}/assignment_policies/999999", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/controllers/api/v1/accounts/inboxes/assignment_policies_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes/assignment_policies_controller_spec.rb new file mode 100644 index 000000000..71ff464f8 --- /dev/null +++ b/spec/controllers/api/v1/accounts/inboxes/assignment_policies_controller_spec.rb @@ -0,0 +1,195 @@ +require 'rails_helper' + +RSpec.describe 'Inbox Assignment Policies API', type: :request do + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:assignment_policy) { create(:assignment_policy, account: account) } + + describe 'GET /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + context 'when inbox has an assignment policy' do + before do + create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) + end + + it 'returns the assignment policy for the inbox' do + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['id']).to eq(assignment_policy.id) + expect(json_response['name']).to eq(assignment_policy.name) + end + end + + context 'when inbox has no assignment policy' do + it 'returns not found' do + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + end + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + params: { assignment_policy_id: assignment_policy.id } + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + it 'assigns a policy to the inbox' do + expect do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + params: { assignment_policy_id: assignment_policy.id }, + headers: admin.create_new_auth_token, + as: :json + end.to change(InboxAssignmentPolicy, :count).by(1) + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['id']).to eq(assignment_policy.id) + end + + it 'replaces existing assignment policy for inbox' do + other_policy = create(:assignment_policy, account: account) + create(:inbox_assignment_policy, inbox: inbox, assignment_policy: other_policy) + + expect do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + params: { assignment_policy_id: assignment_policy.id }, + headers: admin.create_new_auth_token, + as: :json + end.not_to change(InboxAssignmentPolicy, :count) + + expect(response).to have_http_status(:success) + expect(inbox.reload.inbox_assignment_policy.assignment_policy).to eq(assignment_policy) + end + + it 'returns not found for invalid assignment policy' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + params: { assignment_policy_id: 999_999 }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + end + + it 'returns not found for invalid inbox' do + post "/api/v1/accounts/#{account.id}/inboxes/999999/assignment_policy", + params: { assignment_policy_id: assignment_policy.id }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + params: { assignment_policy_id: assignment_policy.id }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated admin' do + let(:admin) { create(:user, account: account, role: :administrator) } + + context 'when inbox has an assignment policy' do + before do + create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) + end + + it 'removes the assignment policy from inbox' do + expect do + delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + headers: admin.create_new_auth_token, + as: :json + end.to change(InboxAssignmentPolicy, :count).by(-1) + + expect(response).to have_http_status(:success) + expect(inbox.reload.inbox_assignment_policy).to be_nil + end + end + + context 'when inbox has no assignment policy' do + it 'returns error' do + expect do + delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + headers: admin.create_new_auth_token, + as: :json + end.not_to change(InboxAssignmentPolicy, :count) + + expect(response).to have_http_status(:not_found) + end + end + + it 'returns not found for invalid inbox' do + delete "/api/v1/accounts/#{account.id}/inboxes/999999/assignment_policy", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + end + end + + context 'when it is an agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/enterprise/models/assignment_policy_spec.rb b/spec/enterprise/models/assignment_policy_spec.rb new file mode 100644 index 000000000..0d21cc3c4 --- /dev/null +++ b/spec/enterprise/models/assignment_policy_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe AssignmentPolicy do + let(:account) { create(:account) } + + describe 'enum values' do + let(:assignment_policy) { create(:assignment_policy, account: account) } + + describe 'assignment_order' do + it 'can be set to balanced' do + assignment_policy.update!(assignment_order: :balanced) + expect(assignment_policy.assignment_order).to eq('balanced') + expect(assignment_policy.round_robin?).to be false + expect(assignment_policy.balanced?).to be true + end + end + end +end diff --git a/spec/factories/assignment_policies.rb b/spec/factories/assignment_policies.rb new file mode 100644 index 000000000..6a696caa4 --- /dev/null +++ b/spec/factories/assignment_policies.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :assignment_policy do + account + sequence(:name) { |n| "Assignment Policy #{n}" } + description { 'Test assignment policy description' } + assignment_order { 0 } + conversation_priority { 0 } + fair_distribution_limit { 10 } + fair_distribution_window { 3600 } + enabled { true } + end +end diff --git a/spec/factories/inbox_assignment_policies.rb b/spec/factories/inbox_assignment_policies.rb new file mode 100644 index 000000000..80bcae223 --- /dev/null +++ b/spec/factories/inbox_assignment_policies.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :inbox_assignment_policy do + inbox + assignment_policy + end +end diff --git a/spec/models/assignment_policy_spec.rb b/spec/models/assignment_policy_spec.rb new file mode 100644 index 000000000..1a97bbda0 --- /dev/null +++ b/spec/models/assignment_policy_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +RSpec.describe AssignmentPolicy do + describe 'associations' do + it { is_expected.to belong_to(:account) } + it { is_expected.to have_many(:inbox_assignment_policies).dependent(:destroy) } + it { is_expected.to have_many(:inboxes).through(:inbox_assignment_policies) } + end + + describe 'validations' do + subject { build(:assignment_policy) } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:account_id) } + end + + describe 'fair distribution validations' do + it 'requires fair_distribution_limit to be greater than 0' do + policy = build(:assignment_policy, fair_distribution_limit: 0) + expect(policy).not_to be_valid + expect(policy.errors[:fair_distribution_limit]).to include('must be greater than 0') + end + + it 'requires fair_distribution_window to be greater than 0' do + policy = build(:assignment_policy, fair_distribution_window: -1) + expect(policy).not_to be_valid + expect(policy.errors[:fair_distribution_window]).to include('must be greater than 0') + end + end + + describe 'enum values' do + let(:assignment_policy) { create(:assignment_policy) } + + describe 'conversation_priority' do + it 'can be set to earliest_created' do + assignment_policy.update!(conversation_priority: :earliest_created) + expect(assignment_policy.conversation_priority).to eq('earliest_created') + expect(assignment_policy.earliest_created?).to be true + end + + it 'can be set to longest_waiting' do + assignment_policy.update!(conversation_priority: :longest_waiting) + expect(assignment_policy.conversation_priority).to eq('longest_waiting') + expect(assignment_policy.longest_waiting?).to be true + end + end + + describe 'assignment_order' do + it 'can be set to round_robin' do + assignment_policy.update!(assignment_order: :round_robin) + expect(assignment_policy.assignment_order).to eq('round_robin') + expect(assignment_policy.round_robin?).to be true + end + end + end +end