Class: HrLite::PayrollRun
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- HrLite::PayrollRun
- Defined in:
- app/models/hr_lite/payroll_run.rb
Overview
Monthly run lifecycle: draft -> processing -> review -> finalized -> published (terminal). Compute is synchronous (pure-Ruby math over a small team; the seam for a background job is one perform_later away).
Constant Summary collapse
- STATUSES =
%w[draft processing review finalized published].freeze
Instance Method Summary collapse
- #compute!(actor:) ⇒ Object
- #editable? ⇒ Boolean
- #finalize!(actor:) ⇒ Object
- #label ⇒ Object
- #publish!(actor:) ⇒ Object
- #total_deductions ⇒ Object
- #total_employer_cost ⇒ Object
-
#total_gross ⇒ Object
Ruby-side aggregates (amounts are encrypted — no SQL sums).
- #total_net ⇒ Object
- #unlock!(actor:) ⇒ Object
Instance Method Details
#compute!(actor:) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 |
# File 'app/models/hr_lite/payroll_run.rb', line 30 def compute!(actor:) raise_unless %w[draft review] update!(status: "processing") PayrollRunProcessor.call(self) update!(status: "review", processed_at: Time.current) true rescue => e update_columns(status: "draft") # rubocop:disable Rails/SkipsModelValidations raise e end |
#editable? ⇒ Boolean
22 23 24 |
# File 'app/models/hr_lite/payroll_run.rb', line 22 def editable? draft? || processing? || review? end |
#finalize!(actor:) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'app/models/hr_lite/payroll_run.rb', line 42 def finalize!(actor:) raise_unless %w[review] raise ActiveRecord::RecordInvalid.new(self), "no slips" if salary_slips.none? update!(status: "finalized", finalized_at: Time.current, finalized_by_id: actor.id) Notifications.publish( "payroll.finalized", title: "Payroll #{label} finalized — #{salary_slips.count} slips, net #{Money.round2(total_net).to_s('F')}", path: "/admin/payroll_runs/#{id}" ) true end |
#label ⇒ Object
26 27 28 |
# File 'app/models/hr_lite/payroll_run.rb', line 26 def label period_month.strftime("%B %Y") end |
#publish!(actor:) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'app/models/hr_lite/payroll_run.rb', line 61 def publish!(actor:) raise_unless %w[finalized] update!(status: "published", published_at: Time.current, published_by_id: actor.id) slips = salary_slips.includes(:user).to_a Notifications.publish( "payroll.published", title: "Your salary slip for #{label} is ready", body: "Open Earthly HR to view or download it.", path: "/salary_slips", bell_to: slips.map(&:user), email_to: slips.map(&:user) ) true end |
#total_deductions ⇒ Object
79 |
# File 'app/models/hr_lite/payroll_run.rb', line 79 def total_deductions = sum_slips(:total_deductions) |
#total_employer_cost ⇒ Object
82 83 84 85 86 |
# File 'app/models/hr_lite/payroll_run.rb', line 82 def total_employer_cost salary_slips.sum(BigDecimal(0)) do |slip| slip.employer_costs_hash.values.sum(BigDecimal(0)) { |v| Money.d(v) } end end |
#total_gross ⇒ Object
Ruby-side aggregates (amounts are encrypted — no SQL sums).
78 |
# File 'app/models/hr_lite/payroll_run.rb', line 78 def total_gross = sum_slips(:gross_earnings) |
#total_net ⇒ Object
80 |
# File 'app/models/hr_lite/payroll_run.rb', line 80 def total_net = sum_slips(:net_pay) |
#unlock!(actor:) ⇒ Object
55 56 57 58 59 |
# File 'app/models/hr_lite/payroll_run.rb', line 55 def unlock!(actor:) raise_unless %w[finalized] update!(status: "review") true end |