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
|
||||
custom_attribute_definition:
|
||||
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:
|
||||
period: Reporting period %{since} to %{until}
|
||||
utc_warning: The report generated is in UTC timezone
|
||||
|
||||
@@ -96,6 +96,12 @@ Rails.application.routes.draw do
|
||||
post :execute, on: :member
|
||||
end
|
||||
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 :campaigns, only: [:index, :create, :show, :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]
|
||||
def change
|
||||
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.
|
||||
|
||||
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
|
||||
enable_extension "pg_stat_statements"
|
||||
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
|
||||
end
|
||||
|
||||
create_table "leaves", force: :cascade do |t|
|
||||
create_table "leave_records", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "user_id", 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 "created_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"], name: "index_leaves_on_account_id"
|
||||
t.index ["approved_by_id"], name: "index_leaves_on_approved_by_id"
|
||||
t.index ["user_id"], name: "index_leaves_on_user_id"
|
||||
t.index ["account_id", "status"], name: "index_leave_records_on_account_id_and_status"
|
||||
t.index ["account_id"], name: "index_leave_records_on_account_id"
|
||||
t.index ["approved_by_id"], name: "index_leave_records_on_approved_by_id"
|
||||
t.index ["user_id"], name: "index_leave_records_on_user_id"
|
||||
end
|
||||
|
||||
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 :applied_slas, 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_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 :copilot_threads, dependent: :destroy_async
|
||||
has_many :leave_records, dependent: :destroy_async
|
||||
end
|
||||
|
||||
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