Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d54f36aa19 | ||
|
|
d739e6e6e8 | ||
|
|
2d320bff17 | ||
|
|
d883d5d58e | ||
|
|
0fe9a3a5e0 | ||
|
|
fe1c29a15e | ||
|
|
081a812243 | ||
|
|
9031c710b7 | ||
|
|
5ce8533e9f | ||
|
|
857c8d90d2 | ||
|
|
d5d13792a8 |
@@ -98,6 +98,9 @@ en:
|
|||||||
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
|
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
|
||||||
custom_attribute_definition:
|
custom_attribute_definition:
|
||||||
key_conflict: The provided key is not allowed as it might conflict with default attributes.
|
key_conflict: The provided key is not allowed as it might conflict with default attributes.
|
||||||
|
leave_records:
|
||||||
|
cannot_update_non_pending: Cannot update non-pending leave record
|
||||||
|
cannot_delete: Cannot delete this leave record
|
||||||
reports:
|
reports:
|
||||||
period: Reporting period %{since} to %{until}
|
period: Reporting period %{since} to %{until}
|
||||||
utc_warning: The report generated is in UTC timezone
|
utc_warning: The report generated is in UTC timezone
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ Rails.application.routes.draw do
|
|||||||
post :execute, on: :member
|
post :execute, on: :member
|
||||||
end
|
end
|
||||||
resources :sla_policies, only: [:index, :create, :show, :update, :destroy]
|
resources :sla_policies, only: [:index, :create, :show, :update, :destroy]
|
||||||
|
resources :leave_records, only: [:index, :create, :show, :update, :destroy] do
|
||||||
|
member do
|
||||||
|
patch :approve
|
||||||
|
patch :reject
|
||||||
|
end
|
||||||
|
end
|
||||||
resources :custom_roles, only: [:index, :create, :show, :update, :destroy]
|
resources :custom_roles, only: [:index, :create, :show, :update, :destroy]
|
||||||
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
|
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
|
||||||
resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
|
resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class CreateLeaves < ActiveRecord::Migration[7.1]
|
class CreateLeaves < ActiveRecord::Migration[7.1]
|
||||||
def change
|
def change
|
||||||
create_table :leaves do |t|
|
create_table :leaves do |t|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class RenameLeaveTableToLeaveRecords < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
rename_table :leaves, :leave_records
|
||||||
|
end
|
||||||
|
end
|
||||||
+6
-6
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
|
ActiveRecord::Schema[7.1].define(version: 2025_08_20_083315) do
|
||||||
# These extensions should be enabled to support this database
|
# These extensions should be enabled to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
@@ -848,7 +848,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
|
|||||||
t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true
|
t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "leaves", force: :cascade do |t|
|
create_table "leave_records", force: :cascade do |t|
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.date "start_date", null: false
|
t.date "start_date", null: false
|
||||||
@@ -860,10 +860,10 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
|
|||||||
t.datetime "approved_at"
|
t.datetime "approved_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["account_id", "status"], name: "index_leaves_on_account_id_and_status"
|
t.index ["account_id", "status"], name: "index_leave_records_on_account_id_and_status"
|
||||||
t.index ["account_id"], name: "index_leaves_on_account_id"
|
t.index ["account_id"], name: "index_leave_records_on_account_id"
|
||||||
t.index ["approved_by_id"], name: "index_leaves_on_approved_by_id"
|
t.index ["approved_by_id"], name: "index_leave_records_on_approved_by_id"
|
||||||
t.index ["user_id"], name: "index_leaves_on_user_id"
|
t.index ["user_id"], name: "index_leave_records_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "macros", force: :cascade do |t|
|
create_table "macros", force: :cascade do |t|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
class Api::V1::Accounts::LeaveRecordsController < Api::V1::Accounts::EnterpriseAccountsController
|
||||||
|
before_action :check_authorization
|
||||||
|
before_action :fetch_leave_record, only: [:show, :update, :destroy, :approve, :reject]
|
||||||
|
before_action :ensure_pending_status, only: [:update]
|
||||||
|
before_action :ensure_can_be_cancelled, only: [:destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@leave_records = policy_scope(Current.account.leave_records)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@leave_record = Current.account.leave_records.create!(permitted_params.merge(user: current_user))
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@leave_record.update!(permitted_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@leave_record.destroy!
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve
|
||||||
|
@leave_record.approve!(current_user)
|
||||||
|
render :show
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject
|
||||||
|
@leave_record.reject!(current_user)
|
||||||
|
render :show
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.require(:leave_record).permit(:start_date, :end_date, :leave_type, :reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_leave_record
|
||||||
|
@leave_record = policy_scope(Current.account.leave_records).find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_pending_status
|
||||||
|
return if @leave_record.pending?
|
||||||
|
|
||||||
|
render_could_not_create_error(I18n.t('errors.leave_records.cannot_update_non_pending'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_can_be_cancelled
|
||||||
|
return if @leave_record.can_be_cancelled?
|
||||||
|
|
||||||
|
render_could_not_create_error(I18n.t('errors.leave_records.cannot_delete'))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,6 +5,7 @@ module Enterprise::Concerns::Account
|
|||||||
has_many :sla_policies, dependent: :destroy_async
|
has_many :sla_policies, dependent: :destroy_async
|
||||||
has_many :applied_slas, dependent: :destroy_async
|
has_many :applied_slas, dependent: :destroy_async
|
||||||
has_many :custom_roles, dependent: :destroy_async
|
has_many :custom_roles, dependent: :destroy_async
|
||||||
|
has_many :leave_records, dependent: :destroy_async
|
||||||
|
|
||||||
has_many :captain_assistants, dependent: :destroy_async, class_name: 'Captain::Assistant'
|
has_many :captain_assistants, dependent: :destroy_async, class_name: 'Captain::Assistant'
|
||||||
has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse'
|
has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ module Enterprise::Concerns::User
|
|||||||
|
|
||||||
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
|
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
|
||||||
has_many :copilot_threads, dependent: :destroy_async
|
has_many :copilot_threads, dependent: :destroy_async
|
||||||
|
has_many :leave_records, dependent: :destroy_async
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_installation_pricing_plan_quantity
|
def ensure_installation_pricing_plan_quantity
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: leave_records
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# approved_at :datetime
|
||||||
|
# end_date :date not null
|
||||||
|
# leave_type :integer default("annual"), not null
|
||||||
|
# reason :text
|
||||||
|
# start_date :date not null
|
||||||
|
# status :integer default("pending"), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
# approved_by_id :bigint
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_leave_records_on_account_id (account_id)
|
||||||
|
# index_leave_records_on_account_id_and_status (account_id,status)
|
||||||
|
# index_leave_records_on_approved_by_id (approved_by_id)
|
||||||
|
# index_leave_records_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
class LeaveRecord < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :approved_by, class_name: 'User', optional: true
|
||||||
|
|
||||||
|
enum leave_type: {
|
||||||
|
annual: 0,
|
||||||
|
sick: 1,
|
||||||
|
personal: 2,
|
||||||
|
maternity: 3,
|
||||||
|
paternity: 4,
|
||||||
|
emergency: 5,
|
||||||
|
bereavement: 6,
|
||||||
|
study: 7,
|
||||||
|
other: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
enum status: {
|
||||||
|
pending: 0,
|
||||||
|
approved: 1,
|
||||||
|
rejected: 2,
|
||||||
|
cancelled: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
validates :start_date, :end_date, presence: true
|
||||||
|
validates :leave_type, :status, presence: true
|
||||||
|
validate :end_date_after_start_date
|
||||||
|
validate :future_dates_for_pending_leaves
|
||||||
|
validate :approved_by_is_admin
|
||||||
|
|
||||||
|
scope :for_account, ->(account_id) { where(account_id: account_id) }
|
||||||
|
scope :for_user, ->(user_id) { where(user_id: user_id) }
|
||||||
|
scope :by_status, ->(status) { where(status: status) }
|
||||||
|
scope :by_leave_type, ->(leave_type) { where(leave_type: leave_type) }
|
||||||
|
scope :in_date_range, ->(start_date, end_date) { where('start_date <= ? AND end_date >= ?', end_date, start_date) }
|
||||||
|
|
||||||
|
def approve!(approved_by_user)
|
||||||
|
update!(status: :approved, approved_by_id: approved_by_user.id, approved_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject!(approved_by_user)
|
||||||
|
update!(status: :rejected, approved_by_id: approved_by_user.id, approved_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def duration_in_days
|
||||||
|
return 0 unless start_date && end_date
|
||||||
|
|
||||||
|
(end_date - start_date).to_i + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_be_cancelled?
|
||||||
|
pending? || (approved? && start_date > Date.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def overlaps_with?(other_leave)
|
||||||
|
return false unless other_leave.is_a?(LeaveRecord)
|
||||||
|
|
||||||
|
start_date <= other_leave.end_date && end_date >= other_leave.start_date
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def end_date_after_start_date
|
||||||
|
return unless start_date && end_date
|
||||||
|
|
||||||
|
errors.add(:end_date, 'must be after start date') if end_date < start_date
|
||||||
|
end
|
||||||
|
|
||||||
|
def future_dates_for_pending_leaves
|
||||||
|
return unless pending?
|
||||||
|
|
||||||
|
errors.add(:start_date, 'must be in the future') if start_date && start_date <= Date.current
|
||||||
|
end
|
||||||
|
|
||||||
|
def approved_by_is_admin
|
||||||
|
return unless approved_by_id && approved_by
|
||||||
|
|
||||||
|
account_user = account.account_users.find_by(user: approved_by)
|
||||||
|
errors.add(:approved_by, 'must be an administrator') unless account_user&.administrator?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
class LeaveRecordPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
@account_user.administrator? || @account_user.agent?
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
@account_user.administrator? || @account_user.agent?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
@account_user.administrator? || @account_user.agent?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
@account_user.administrator? || @account_user.agent?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
@account_user.administrator? || @account_user.agent?
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
|
class Scope
|
||||||
|
attr_reader :user_context, :user, :scope, :account, :account_user
|
||||||
|
|
||||||
|
def initialize(user_context, scope)
|
||||||
|
@user_context = user_context
|
||||||
|
@user = user_context[:user]
|
||||||
|
@account = user_context[:account]
|
||||||
|
@account_user = user_context[:account_user]
|
||||||
|
@scope = scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve
|
||||||
|
if @account_user.administrator?
|
||||||
|
scope.includes(:user, :approved_by)
|
||||||
|
else
|
||||||
|
scope.where(user: @user).includes(:user, :approved_by)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
json.partial! 'api/v1/models/leave_record', formats: [:json], leave_record: @leave_record
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
json.array! @leave_records do |leave_record|
|
||||||
|
json.partial! 'api/v1/models/leave_record', formats: [:json], leave_record: leave_record
|
||||||
|
end
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
json.partial! 'api/v1/models/leave_record', formats: [:json], leave_record: @leave_record
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
json.partial! 'api/v1/models/leave_record', formats: [:json], leave_record: @leave_record
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
json.id leave_record.id
|
||||||
|
json.start_date leave_record.start_date
|
||||||
|
json.end_date leave_record.end_date
|
||||||
|
json.leave_type leave_record.leave_type
|
||||||
|
json.status leave_record.status
|
||||||
|
json.reason leave_record.reason
|
||||||
|
json.duration_in_days leave_record.duration_in_days
|
||||||
|
json.approved_at leave_record.approved_at&.to_i
|
||||||
|
json.created_at leave_record.created_at.to_i
|
||||||
|
json.updated_at leave_record.updated_at.to_i
|
||||||
|
|
||||||
|
json.user do
|
||||||
|
json.id leave_record.user.id
|
||||||
|
json.name leave_record.user.name
|
||||||
|
json.email leave_record.user.email
|
||||||
|
end
|
||||||
|
|
||||||
|
if leave_record.approved_by.present?
|
||||||
|
json.approved_by do
|
||||||
|
json.id leave_record.approved_by.id
|
||||||
|
json.name leave_record.approved_by.name
|
||||||
|
json.email leave_record.approved_by.email
|
||||||
|
end
|
||||||
|
else
|
||||||
|
json.approved_by nil
|
||||||
|
end
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'LeaveRecords API', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
let(:other_agent) { create(:user, account: account, role: :agent) }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/accounts/:account_id/leave_records' do
|
||||||
|
before do
|
||||||
|
create(:leave_record, account: account, user: agent)
|
||||||
|
create(:leave_record, account: account, user: other_agent)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows admins to see all leave records' do
|
||||||
|
get "/api/v1/accounts/#{account.id}/leave_records",
|
||||||
|
headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(json_response.length).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows agents to see only their own leave records' do
|
||||||
|
get "/api/v1/accounts/#{account.id}/leave_records",
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(json_response.length).to eq(1)
|
||||||
|
expect(json_response.first['user']['id']).to eq(agent.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/accounts/:account_id/leave_records' do
|
||||||
|
let(:valid_params) do
|
||||||
|
{
|
||||||
|
leave_record: {
|
||||||
|
start_date: 1.week.from_now.to_date,
|
||||||
|
end_date: 2.weeks.from_now.to_date,
|
||||||
|
leave_type: 'annual',
|
||||||
|
reason: 'Family vacation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a leave record for the current user' do
|
||||||
|
expect do
|
||||||
|
post "/api/v1/accounts/#{account.id}/leave_records",
|
||||||
|
params: valid_params,
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
end.to change(LeaveRecord, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(json_response['user']['id']).to eq(agent.id)
|
||||||
|
expect(json_response['status']).to eq('pending')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails with invalid params' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/leave_records",
|
||||||
|
params: { leave_record: { start_date: nil } },
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /api/v1/accounts/:account_id/leave_records/:id' do
|
||||||
|
let(:leave_record) { create(:leave_record, account: account, user: agent, status: :pending) }
|
||||||
|
let(:update_params) { { leave_record: { reason: 'Updated reason' } } }
|
||||||
|
|
||||||
|
it 'allows updating pending leave records' do
|
||||||
|
patch "/api/v1/accounts/#{account.id}/leave_records/#{leave_record.id}",
|
||||||
|
params: update_params,
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(json_response['reason']).to eq('Updated reason')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'prevents updating approved leave records' do
|
||||||
|
leave_record.update!(status: :approved)
|
||||||
|
|
||||||
|
patch "/api/v1/accounts/#{account.id}/leave_records/#{leave_record.id}",
|
||||||
|
params: update_params,
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json_response['error']).to eq(I18n.t('errors.leave_records.cannot_update_non_pending'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /api/v1/accounts/:account_id/leave_records/:id' do
|
||||||
|
let!(:leave_record) { create(:leave_record, account: account, user: agent, status: :pending) }
|
||||||
|
|
||||||
|
it 'allows deleting cancellable leave records' do
|
||||||
|
expect do
|
||||||
|
delete "/api/v1/accounts/#{account.id}/leave_records/#{leave_record.id}",
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
end.to change(LeaveRecord, :count).by(-1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'prevents deleting non-cancellable leave records' do
|
||||||
|
leave_record.update!(status: :approved, start_date: Date.current, end_date: Date.current + 2.days)
|
||||||
|
|
||||||
|
delete "/api/v1/accounts/#{account.id}/leave_records/#{leave_record.id}",
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json_response['error']).to eq(I18n.t('errors.leave_records.cannot_delete'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /api/v1/accounts/:account_id/leave_records/:id/approve' do
|
||||||
|
let(:leave_record) { create(:leave_record, account: account, user: agent, status: :pending) }
|
||||||
|
|
||||||
|
it 'allows admins to approve leave records' do
|
||||||
|
patch "/api/v1/accounts/#{account.id}/leave_records/#{leave_record.id}/approve",
|
||||||
|
headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(json_response['status']).to eq('approved')
|
||||||
|
expect(json_response['approved_by']['id']).to eq(admin.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'denies agents from approving leave records' do
|
||||||
|
patch "/api/v1/accounts/#{account.id}/leave_records/#{leave_record.id}/approve",
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /api/v1/accounts/:account_id/leave_records/:id/reject' do
|
||||||
|
let(:leave_record) { create(:leave_record, account: account, user: agent, status: :pending) }
|
||||||
|
|
||||||
|
it 'allows admins to reject leave records' do
|
||||||
|
patch "/api/v1/accounts/#{account.id}/leave_records/#{leave_record.id}/reject",
|
||||||
|
headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(json_response['status']).to eq('rejected')
|
||||||
|
expect(json_response['approved_by']['id']).to eq(admin.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def json_response
|
||||||
|
JSON.parse(response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe LeaveRecord, type: :model do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account: account) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
|
||||||
|
describe 'associations' do
|
||||||
|
it { is_expected.to belong_to(:account) }
|
||||||
|
it { is_expected.to belong_to(:user) }
|
||||||
|
it { is_expected.to belong_to(:approved_by).class_name('User').optional }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'validations' do
|
||||||
|
subject { build(:leave_record, account: account, user: user) }
|
||||||
|
|
||||||
|
it { is_expected.to validate_presence_of(:start_date) }
|
||||||
|
it { is_expected.to validate_presence_of(:end_date) }
|
||||||
|
it { is_expected.to validate_presence_of(:leave_type) }
|
||||||
|
it { is_expected.to validate_presence_of(:status) }
|
||||||
|
|
||||||
|
it 'validates end_date is after start_date' do
|
||||||
|
leave = build(:leave_record, account: account, user: user,
|
||||||
|
start_date: Date.current + 2.days,
|
||||||
|
end_date: Date.current)
|
||||||
|
expect(leave).not_to be_valid
|
||||||
|
expect(leave.errors[:end_date]).to include('must be after start date')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'validates future dates for pending leaves' do
|
||||||
|
leave = build(:leave_record, account: account, user: user,
|
||||||
|
status: :pending,
|
||||||
|
start_date: Date.current)
|
||||||
|
expect(leave).not_to be_valid
|
||||||
|
expect(leave.errors[:start_date]).to include('must be in the future')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'validates approved_by is admin when present' do
|
||||||
|
agent = create(:user, account: account, role: :agent)
|
||||||
|
leave = build(:leave_record, account: account, user: user,
|
||||||
|
approved_by: agent,
|
||||||
|
status: :approved)
|
||||||
|
expect(leave).not_to be_valid
|
||||||
|
expect(leave.errors[:approved_by]).to include('must be an administrator')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#duration_in_days' do
|
||||||
|
it 'calculates correct duration' do
|
||||||
|
leave = build(:leave_record,
|
||||||
|
start_date: Date.current + 1.day,
|
||||||
|
end_date: Date.current + 5.days)
|
||||||
|
expect(leave.duration_in_days).to eq(5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#can_be_cancelled?' do
|
||||||
|
it 'returns true for pending leaves' do
|
||||||
|
leave = build(:leave_record, status: :pending)
|
||||||
|
expect(leave.can_be_cancelled?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for approved future leaves' do
|
||||||
|
leave = build(:leave_record, status: :approved,
|
||||||
|
start_date: Date.current + 1.day)
|
||||||
|
expect(leave.can_be_cancelled?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for approved ongoing leaves' do
|
||||||
|
leave = build(:leave_record, status: :approved,
|
||||||
|
start_date: Date.current)
|
||||||
|
expect(leave.can_be_cancelled?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for rejected leaves' do
|
||||||
|
leave = build(:leave_record, status: :rejected)
|
||||||
|
expect(leave.can_be_cancelled?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#approve!' do
|
||||||
|
let(:leave) { create(:leave_record, account: account, user: user, status: :pending) }
|
||||||
|
|
||||||
|
it 'approves the leave and sets approved_by' do
|
||||||
|
freeze_time do
|
||||||
|
leave.approve!(admin)
|
||||||
|
|
||||||
|
expect(leave.status).to eq('approved')
|
||||||
|
expect(leave.approved_by).to eq(admin)
|
||||||
|
expect(leave.approved_at).to eq(Time.current)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#reject!' do
|
||||||
|
let(:leave) { create(:leave_record, account: account, user: user, status: :pending) }
|
||||||
|
|
||||||
|
it 'rejects the leave and sets approved_by' do
|
||||||
|
freeze_time do
|
||||||
|
leave.reject!(admin)
|
||||||
|
|
||||||
|
expect(leave.status).to eq('rejected')
|
||||||
|
expect(leave.approved_by).to eq(admin)
|
||||||
|
expect(leave.approved_at).to eq(Time.current)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#overlaps_with?' do
|
||||||
|
let(:leave1) do
|
||||||
|
build(:leave_record,
|
||||||
|
start_date: Date.current + 5.days,
|
||||||
|
end_date: Date.current + 10.days)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'detects overlapping leaves' do
|
||||||
|
leave2 = build(:leave_record,
|
||||||
|
start_date: Date.current + 7.days,
|
||||||
|
end_date: Date.current + 12.days)
|
||||||
|
expect(leave1.overlaps_with?(leave2)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for non-overlapping leaves' do
|
||||||
|
leave2 = build(:leave_record,
|
||||||
|
start_date: Date.current + 11.days,
|
||||||
|
end_date: Date.current + 15.days)
|
||||||
|
expect(leave1.overlaps_with?(leave2)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe LeaveRecordPolicy, type: :policy do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
let(:leave_record) { create(:leave_record, account: account, user: agent) }
|
||||||
|
|
||||||
|
describe 'permissions' do
|
||||||
|
context 'when user is administrator' do
|
||||||
|
let(:policy) { described_class.new(pundit_context(admin), leave_record) }
|
||||||
|
|
||||||
|
it 'permits all actions' do
|
||||||
|
expect(policy.index?).to be true
|
||||||
|
expect(policy.show?).to be true
|
||||||
|
expect(policy.create?).to be true
|
||||||
|
expect(policy.update?).to be true
|
||||||
|
expect(policy.destroy?).to be true
|
||||||
|
expect(policy.approve?).to be true
|
||||||
|
expect(policy.reject?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is agent' do
|
||||||
|
let(:policy) { described_class.new(pundit_context(agent), leave_record) }
|
||||||
|
|
||||||
|
it 'permits basic actions' do
|
||||||
|
expect(policy.index?).to be true
|
||||||
|
expect(policy.show?).to be true
|
||||||
|
expect(policy.create?).to be true
|
||||||
|
expect(policy.update?).to be true
|
||||||
|
expect(policy.destroy?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'denies approval actions' do
|
||||||
|
expect(policy.approve?).to be false
|
||||||
|
expect(policy.reject?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'scope' do
|
||||||
|
let!(:agent_leave) { create(:leave_record, account: account, user: agent) }
|
||||||
|
let!(:other_agent) { create(:user, account: account, role: :agent) }
|
||||||
|
let!(:other_leave) { create(:leave_record, account: account, user: other_agent) }
|
||||||
|
|
||||||
|
it 'returns all leaves for administrators' do
|
||||||
|
scope = described_class::Scope.new(pundit_context(admin), LeaveRecord).resolve
|
||||||
|
expect(scope).to include(agent_leave, other_leave)
|
||||||
|
expect(scope.count).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only own leaves for agents' do
|
||||||
|
scope = described_class::Scope.new(pundit_context(agent), LeaveRecord).resolve
|
||||||
|
expect(scope).to include(agent_leave)
|
||||||
|
expect(scope).not_to include(other_leave)
|
||||||
|
expect(scope.count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes associations for performance' do
|
||||||
|
scope = described_class::Scope.new(pundit_context(admin), LeaveRecord).resolve
|
||||||
|
expect(scope.includes_values).to include(:user, :approved_by)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def pundit_context(user)
|
||||||
|
{
|
||||||
|
user: user,
|
||||||
|
account: account,
|
||||||
|
account_user: user.account_users.find_by(account: account)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :leave_record do
|
||||||
|
account
|
||||||
|
user
|
||||||
|
start_date { 1.week.from_now.to_date }
|
||||||
|
end_date { 2.weeks.from_now.to_date }
|
||||||
|
leave_type { :annual }
|
||||||
|
status { :pending }
|
||||||
|
reason { 'Annual vacation leave' }
|
||||||
|
|
||||||
|
trait :sick do
|
||||||
|
leave_type { :sick }
|
||||||
|
reason { 'Sick leave for medical treatment' }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :emergency do
|
||||||
|
leave_type { :emergency }
|
||||||
|
reason { 'Emergency family matter' }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :other do
|
||||||
|
leave_type { :other }
|
||||||
|
reason { 'Other type of leave' }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :approved do
|
||||||
|
status { :approved }
|
||||||
|
approved_by { association(:user) }
|
||||||
|
approved_at { 1.day.ago }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :rejected do
|
||||||
|
status { :rejected }
|
||||||
|
approved_by { association(:user) }
|
||||||
|
approved_at { 1.day.ago }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :past_dates do
|
||||||
|
start_date { 2.weeks.ago.to_date }
|
||||||
|
end_date { 1.week.ago.to_date }
|
||||||
|
status { :approved } # Past dates should only be used with approved status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user