Class: HrLite::LeaveBalance
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- HrLite::LeaveBalance
- Defined in:
- app/models/hr_lite/leave_balance.rb
Overview
Hybrid balance: only carry-in and manual adjustments are stored;
entitlement accrues as a pure function of the policy and used is
recomputed live from approved requests — so a holiday added after an
approval self-heals both the quota and payroll.
year is a LeaveYear key (calendar year by default; with
config.leave_year_start_month = 7, key 2026 = Jul 2026 – Jun 2027).
Entitlement prorates from the joining date, Keka-style: joined on or
before the 15th → that month counts; after the 15th → from next month.
Class Method Summary collapse
-
.adjust!(user, leave_type, year, delta:, note:) ⇒ Object
The single write path for the stored adjustment.
- .for(user, leave_type, year) ⇒ Object
-
.lock_for(user, leave_type, year) ⇒ Object
Persisted and row-locked so concurrent writers serialize here.
Instance Method Summary collapse
Class Method Details
.adjust!(user, leave_type, year, delta:, note:) ⇒ Object
The single write path for the stored adjustment. Both callers — the comp-off credit and the admin correction — increment the same column, and read-modify-write without the lock lost one of them.
40 41 42 43 44 45 46 47 48 |
# File 'app/models/hr_lite/leave_balance.rb', line 40 def self.adjust!(user, leave_type, year, delta:, note:) transaction do balance = lock_for(user, leave_type, year) balance.adjustment += delta balance.adjustment_note = [ balance.adjustment_note.presence, note ].compact.join("; ") balance.save! balance end end |
.for(user, leave_type, year) ⇒ Object
18 19 20 |
# File 'app/models/hr_lite/leave_balance.rb', line 18 def self.for(user, leave_type, year) find_or_initialize_by(user_id: user.id, leave_type: leave_type, year: year) end |
.lock_for(user, leave_type, year) ⇒ Object
Persisted and row-locked so concurrent writers serialize here. The create sits in its own savepoint: on PostgreSQL a failed INSERT aborts the enclosing transaction, so a plain rescue could never run.
25 26 27 28 29 30 31 32 33 34 35 |
# File 'app/models/hr_lite/leave_balance.rb', line 25 def self.lock_for(user, leave_type, year) attributes = { user_id: user.id, leave_type_id: leave_type.id, year: year } balance = find_by(attributes) balance ||= begin transaction(requires_new: true) { create!(attributes) } rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid find_by!(attributes) end balance.lock! balance end |
Instance Method Details
#available(as_of: Date.current) ⇒ Object
69 70 71 |
# File 'app/models/hr_lite/leave_balance.rb', line 69 def available(as_of: Date.current) entitled(as_of: as_of) - used end |
#entitled(as_of: Date.current) ⇒ Object
50 51 52 53 54 |
# File 'app/models/hr_lite/leave_balance.rb', line 50 def entitled(as_of: Date.current) return Float::INFINITY if leave_type.unlimited? (accrued_base(as_of) + carried_forward + adjustment).round(1) end |
#used ⇒ Object
56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'app/models/hr_lite/leave_balance.rb', line 56 def used range = LeaveYear.range(year) requests = LeaveRequest.approved .where(user_id: user_id, leave_type_id: leave_type_id) .where(start_date: range) # ONE calendar for the whole leave year, reused across every request. # Building one per request meant a Holiday query and a Setting query # each — and the admin balances grid reads this cell for every employee # times every leave type. calendar = WorkingCalendar.new(range) requests.sum { |request| LeaveDayCounter.count(request, calendar: calendar) } end |