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
processingis allowed back in so a run stranded there by a killed process (deploy roll, timeout) can be recomputed instead of blocking that month forever. - #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
processing is allowed back in so a run stranded there by a killed
process (deploy roll, timeout) can be recomputed instead of blocking
that month forever.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'app/models/hr_lite/payroll_run.rb', line 35 def compute!(actor:) raise_unless %w[draft review processing] previous = status begin update!(status: "processing") PayrollRunProcessor.call(self) update!(status: "review", processed_at: Time.current) true rescue => e # Restore what the run WAS. Hardcoding "draft" here used to demote a # finalized or published run — which then exposed the delete-draft # control, and that cascades over every salary slip. update_columns(status: previous) # rubocop:disable Rails/SkipsModelValidations raise e end end |
#editable? ⇒ Boolean
24 25 26 |
# File 'app/models/hr_lite/payroll_run.rb', line 24 def editable? draft? || processing? || review? end |
#finalize!(actor:) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'app/models/hr_lite/payroll_run.rb', line 53 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
28 29 30 |
# File 'app/models/hr_lite/payroll_run.rb', line 28 def label period_month.strftime("%B %Y") end |
#publish!(actor:) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'app/models/hr_lite/payroll_run.rb', line 72 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 # Everyone is emailed — a final settlement matters most to the person who # has left — but a bell is only useful to someone who can still sign in, # and offboarding revokes that. Sending one pointed at a page they cannot # open is just noise in a place they will never look. exited = EmployeeProfile.where(user_id: slips.map(&:user_id)) .where(date_of_exit: ..Date.current) .pluck(:user_id).to_set 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.reject { |slip| exited.include?(slip.user_id) }.map(&:user), email_to: slips.map(&:user) ) true end |
#total_deductions ⇒ Object
98 |
# File 'app/models/hr_lite/payroll_run.rb', line 98 def total_deductions = sum_slips(:total_deductions) |
#total_employer_cost ⇒ Object
101 102 103 104 105 |
# File 'app/models/hr_lite/payroll_run.rb', line 101 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).
97 |
# File 'app/models/hr_lite/payroll_run.rb', line 97 def total_gross = sum_slips(:gross_earnings) |
#total_net ⇒ Object
99 |
# File 'app/models/hr_lite/payroll_run.rb', line 99 def total_net = sum_slips(:net_pay) |
#unlock!(actor:) ⇒ Object
66 67 68 69 70 |
# File 'app/models/hr_lite/payroll_run.rb', line 66 def unlock!(actor:) raise_unless %w[finalized] update!(status: "review") true end |