diff --git a/app/models/account.rb b/app/models/account.rb index b415680ee..331343de1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -149,4 +149,5 @@ class Account < ApplicationRecord end Account.prepend_mod_with('Account') +Account.include_mod_with('EnterpriseAccountConcern') Account.include_mod_with('Audit::Account') diff --git a/config/routes.rb b/config/routes.rb index 3cb03a1de..bdad7c1bf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,6 +64,7 @@ Rails.application.routes.draw do post :execute, on: :member post :attach_file, on: :collection end + resources :sla_policies, only: [:index, :create, :show, :update, :destroy] resources :campaigns, only: [:index, :create, :show, :update, :destroy] resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy] namespace :channels do diff --git a/db/migrate/20230503101201_create_sla_policies.rb b/db/migrate/20230503101201_create_sla_policies.rb new file mode 100644 index 000000000..9c0abce25 --- /dev/null +++ b/db/migrate/20230503101201_create_sla_policies.rb @@ -0,0 +1,13 @@ +class CreateSlaPolicies < ActiveRecord::Migration[6.1] + def change + create_table :sla_policies do |t| + t.string :name, null: false + t.float :frt_threshold, default: nil + t.float :rt_threshold, default: nil + t.boolean 'only_during_business_hours', default: false + t.references :account, index: true, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 459e97320..befec82f7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -804,6 +804,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_10_113208) do t.index ["user_id"], name: "index_reporting_events_on_user_id" end + create_table "sla_policies", force: :cascade do |t| + t.string "name", null: false + t.float "frt_threshold" + t.float "rt_threshold" + t.boolean "only_during_business_hours", default: false + t.bigint "account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_sla_policies_on_account_id" + end + create_table "taggings", id: :serial, force: :cascade do |t| t.integer "tag_id" t.string "taggable_type" diff --git a/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb index d1e6b9287..b2dca5ce6 100644 --- a/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb @@ -1,16 +1,9 @@ -# module Enterprise::Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::BaseController -class Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::EnterpriseAccountsController before_action :check_admin_authorization? before_action :fetch_audit - before_action :prepend_view_paths RESULTS_PER_PAGE = 15 - # Prepend the view path to the enterprise/app/views won't be available by default - def prepend_view_paths - prepend_view_path 'enterprise/app/views/' - end - def show @audit_logs = @audit_logs.page(params[:page]).per(RESULTS_PER_PAGE) @current_page = @audit_logs.current_page diff --git a/enterprise/app/controllers/api/v1/accounts/enterprise_accounts_controller.rb b/enterprise/app/controllers/api/v1/accounts/enterprise_accounts_controller.rb new file mode 100644 index 000000000..f3028721c --- /dev/null +++ b/enterprise/app/controllers/api/v1/accounts/enterprise_accounts_controller.rb @@ -0,0 +1,8 @@ +class Api::V1::Accounts::EnterpriseAccountsController < Api::V1::Accounts::BaseController + before_action :prepend_view_paths + + # Prepend the view path to the enterprise/app/views won't be available by default + def prepend_view_paths + prepend_view_path 'enterprise/app/views/' + end +end diff --git a/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb b/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb new file mode 100644 index 000000000..fa79c5362 --- /dev/null +++ b/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb @@ -0,0 +1,31 @@ +class Api::V1::Accounts::SlaPoliciesController < Api::V1::Accounts::EnterpriseAccountsController + before_action :fetch_sla, only: [:show, :update, :destroy] + before_action :check_authorization + + def index + @sla_policies = Current.account.sla_policies + end + + def create + @sla_policy = Current.account.sla_policies.create!(permitted_params) + end + + def show; end + + def update + @sla_policy.update!(permitted_params) + end + + def destroy + @sla_policy.destroy! + head :ok + end + + def permitted_params + params.require(:sla_policy).permit(:name, :rt_threshold, :frt_threshold, :only_during_business_hours) + end + + def fetch_sla + @sla_policy = Current.account.sla_policies.find_by(id: params[:id]) + end +end diff --git a/enterprise/app/models/enterprise/enterprise_account_concern.rb b/enterprise/app/models/enterprise/enterprise_account_concern.rb new file mode 100644 index 000000000..8e430dbe7 --- /dev/null +++ b/enterprise/app/models/enterprise/enterprise_account_concern.rb @@ -0,0 +1,7 @@ +module Enterprise::EnterpriseAccountConcern + extend ActiveSupport::Concern + + included do + has_many :sla_policies, dependent: :destroy_async + end +end diff --git a/enterprise/app/models/sla_policy.rb b/enterprise/app/models/sla_policy.rb new file mode 100644 index 000000000..042cc2444 --- /dev/null +++ b/enterprise/app/models/sla_policy.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: sla_policies +# +# id :bigint not null, primary key +# frt_threshold :float +# name :string not null +# only_during_business_hours :boolean default(FALSE) +# rt_threshold :float +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# +# Indexes +# +# index_sla_policies_on_account_id (account_id) +# +class SlaPolicy < ApplicationRecord + belongs_to :account + validates :name, presence: true +end diff --git a/enterprise/app/policies/sla_policy_policy.rb b/enterprise/app/policies/sla_policy_policy.rb new file mode 100644 index 000000000..4a2ca6503 --- /dev/null +++ b/enterprise/app/policies/sla_policy_policy.rb @@ -0,0 +1,21 @@ +class SlaPolicyPolicy < ApplicationPolicy + def index? + @account_user.administrator? || @account_user.agent? + end + + def update? + @account_user.administrator? + end + + def show? + @account_user.administrator? || @account_user.agent? + end + + def create? + @account_user.administrator? + end + + def destroy? + @account_user.administrator? + end +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/create.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/create.json.jbuilder new file mode 100644 index 000000000..5ecafcd2c --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/create.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: @sla_policy +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/index.json.jbuilder new file mode 100644 index 000000000..45534cc86 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/index.json.jbuilder @@ -0,0 +1,5 @@ +json.payload do + json.array! @sla_policies do |sla_policy| + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: sla_policy + end +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/show.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/show.json.jbuilder new file mode 100644 index 000000000..5ecafcd2c --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/show.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: @sla_policy +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/update.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/update.json.jbuilder new file mode 100644 index 000000000..5ecafcd2c --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/update.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: @sla_policy +end diff --git a/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder b/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder new file mode 100644 index 000000000..cd03d50ad --- /dev/null +++ b/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder @@ -0,0 +1,5 @@ +json.id sla_policy.id +json.name sla_policy.name +json.frt_threshold sla_policy.frt_threshold +json.rt_threshold sla_policy.rt_threshold +json.only_during_business_hours sla_policy.only_during_business_hours diff --git a/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb new file mode 100644 index 000000000..99a3bf0f0 --- /dev/null +++ b/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb @@ -0,0 +1,189 @@ +require 'rails_helper' + +RSpec.describe 'Enterprise SLA API', type: :request do + let(:account) { create(:account) } + let(:administrator) { create(:user, account: account, role: :administrator) } + let(:agent) { create(:user, account: account, role: :agent) } + + before do + create(:sla_policy, account: account, name: 'SLA 1') + end + + describe 'GET #index' do + context 'when it is an authenticated user' do + it 'returns all slas in the account' do + get "/api/v1/accounts/#{account.id}/sla_policies", + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload'][0]).to include('name' => 'SLA 1') + end + end + + context 'when the user is an agent' do + it 'returns slas in the account' do + get "/api/v1/accounts/#{account.id}/sla_policies", + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload'][0]).to include('name' => 'SLA 1') + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'GET #show' do + let(:sla_policy) { create(:sla_policy, account: account) } + + context 'when it is an authenticated user' do + it 'shows the sla' do + get "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => sla_policy.name) + end + end + + context 'when the user is an agent' do + it 'shows the sla details' do + get "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => sla_policy.name) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST #create' do + let(:valid_params) do + { sla_policy: { name: 'SLA 2', + frt_threshold: 1000, + rt_threshold: 1000, + only_during_business_hours: false } } + end + + context 'when it is an authenticated user' do + it 'creates the sla_policy' do + expect do + post "/api/v1/accounts/#{account.id}/sla_policies", params: valid_params, + headers: administrator.create_new_auth_token + end.to change(SlaPolicy, :count).by(1) + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => 'SLA 2') + end + end + + context 'when the user is an agent' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies", + params: valid_params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'PUT #update' do + let(:sla_policy) { create(:sla_policy, account: account) } + + context 'when it is an authenticated user' do + it 'updates the sla_policy' do + put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + params: { sla_policy: { name: 'SLA 2' } }, + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => 'SLA 2') + end + end + + context 'when the user is an agent' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + params: { sla_policy: { name: 'SLA 2' } }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE #destroy' do + let(:sla_policy) { create(:sla_policy, account: account) } + + context 'when it is an authenticated user' do + it 'deletes the sla_policy' do + delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + expect(SlaPolicy.count).to eq(1) + end + end + + context 'when the user is an agent' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}" + + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/enterprise/models/account_spec.rb b/spec/enterprise/models/account_spec.rb index 4c84e3465..af73da48f 100644 --- a/spec/enterprise/models/account_spec.rb +++ b/spec/enterprise/models/account_spec.rb @@ -3,6 +3,24 @@ require 'rails_helper' RSpec.describe Account do + include ActiveJob::TestHelper + + describe 'sla_policies' do + let!(:account) { create(:account) } + let!(:sla_policy) { create(:sla_policy, account: account) } + + it 'returns associated sla policies' do + expect(account.sla_policies).to eq([sla_policy]) + end + + it 'deletes associated sla policies' do + perform_enqueued_jobs do + account.destroy! + end + expect { sla_policy.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + describe 'usage_limits' do before do create(:installation_config, name: 'ACCOUNT_AGENTS_LIMIT', value: 20) diff --git a/spec/enterprise/models/sla_policy_spec.rb b/spec/enterprise/models/sla_policy_spec.rb new file mode 100644 index 000000000..6be1f4e45 --- /dev/null +++ b/spec/enterprise/models/sla_policy_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe SlaPolicy, type: :model do + include ActiveJob::TestHelper + let(:account) { create(:account) } + let(:admin) { create(:user, account: account, role: :administrator) } + + describe 'validations' do + it { is_expected.to validate_presence_of(:name) } + end + + describe 'associations' do + it { is_expected.to belong_to(:account) } + end + + describe 'validates_factory' do + it 'creates valid sla policy object' do + sla_policy = create(:sla_policy) + expect(sla_policy.name).to eq 'sla_1' + end + end +end diff --git a/spec/factories/sla_policies.rb b/spec/factories/sla_policies.rb new file mode 100644 index 000000000..3f1d33b43 --- /dev/null +++ b/spec/factories/sla_policies.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :sla_policy do + account + name { 'sla_1' } + rt_threshold { 1000 } + frt_threshold { 2000 } + only_during_business_hours { false } + end +end