Module: HrLite::StatutoryRateCard

Defined in:
lib/hr_lite/statutory_rate_card.rb

Overview

Where payroll asks "what were the statutory figures in this month".

Cards live in the DATABASE (hr_lite_statutory_rate_cards), effective- dated, so adding a financial year is a screen an accountant fills in rather than a gem release. Every April used to be a deadline the gem controlled and the company did not.

CARDS below is the SEED — the figures the gem ships with, copied into the table by hr_lite:seed and then owned by the install. It is also the fallback for a host that has not migrated or seeded yet, so the lookup never returns nil half-way through computing somebody's salary.

When no card covers a run's financial year the lookup still answers — payroll cannot stop dead every 1 April — but warning_for says so, and PayrollRunProcessor puts that sentence above every other warning.

Constant Summary collapse

CARDS =
{
  Date.new(2025, 4, 1) => {
    pf: {
      employee_rate: r("0.12"), employer_rate: r("0.12"), eps_rate: r("0.0833"),
      wage_ceiling: r("15000"), eps_wage_ceiling: r("15000"),
      edli_rate: r("0.005"), edli_ceiling: r("15000"), admin_rate: r("0.005")
    },
    esi: { employee_rate: r("0.0075"), employer_rate: r("0.0325"), gross_ceiling: r("21000") },
    pt: {
      # Neither UP nor Uttarakhand levies professional tax today; both
      # ship empty (PT = 0). Karnataka included as a worked template.
      "none" => [],
      "uttar_pradesh" => [],
      "uttarakhand" => [],
      # Inclusive lower bounds. `above: 24999` with a strict `>` taxed a
      # prorated gross of ₹24,999.50, which is below the real threshold.
      "karnataka" => [ { from: r("25000"), monthly: r("200") } ]
    },
    income_tax: {
      "new" => {
        standard_deduction: r("75000"), rebate_cap: r("1200000"), cess_rate: r("0.04"),
        # §115BAC carries marginal relief just above the rebate cap; the
        # old regime does not, so this is a per-regime flag.
        marginal_relief: true,
        slabs: [
          [ r("0"), r("400000"), r("0") ],
          [ r("400000"), r("800000"), r("0.05") ],
          [ r("800000"), r("1200000"), r("0.10") ],
          [ r("1200000"), r("1600000"), r("0.15") ],
          [ r("1600000"), r("2000000"), r("0.20") ],
          [ r("2000000"), r("2400000"), r("0.25") ],
          [ r("2400000"), nil, r("0.30") ]
        ]
      },
      "old" => {
        standard_deduction: r("50000"), rebate_cap: r("500000"), cess_rate: r("0.04"),
        slabs: [
          [ r("0"), r("250000"), r("0") ],
          [ r("250000"), r("500000"), r("0.05") ],
          [ r("500000"), r("1000000"), r("0.20") ],
          [ r("1000000"), nil, r("0.30") ]
        ]
      }
    }
  }
}.freeze

Class Method Summary collapse

Class Method Details

.earliest_dateObject



103
104
105
# File 'lib/hr_lite/statutory_rate_card.rb', line 103

def self.earliest_date
  stored? ? StatutoryRateCardRecord.chronological.first.effective_from : CARDS.keys.min
end

.effective_date_for(period_month) ⇒ Object

Which card a run will actually use. A month older than every card borrows the earliest one — see predates_cards?, which says so.



95
96
97
98
99
100
101
# File 'lib/hr_lite/statutory_rate_card.rb', line 95

def self.effective_date_for(period_month)
  if stored?
    return StatutoryRateCardRecord.effective_for(period_month).effective_from
  end

  CARDS.keys.sort.reverse.find { |date| date <= period_month } || CARDS.keys.min
end

.for(period_month) ⇒ Object



68
69
70
71
72
73
# File 'lib/hr_lite/statutory_rate_card.rb', line 68

def self.for(period_month)
  record = record_for(period_month)
  return record.to_card if record

  CARDS[effective_date_for(period_month)]
end

.predates_cards?(period_month) ⇒ Boolean

The run is older than every card we ship, so it is being computed on rates that had not been announced yet.

Returns:

  • (Boolean)


116
117
118
# File 'lib/hr_lite/statutory_rate_card.rb', line 116

def self.predates_cards?(period_month)
  period_month < earliest_date
end

.record_for(period_month) ⇒ Object

The stored card a run resolves to, or nil when the table is empty (a host that has not run hr_lite:seed yet) and the shipped hash answers instead.



78
79
80
81
82
# File 'lib/hr_lite/statutory_rate_card.rb', line 78

def self.record_for(period_month)
  return nil unless stored?

  StatutoryRateCardRecord.effective_for(period_month)
end

.stale_for?(period_month) ⇒ Boolean

The card in force is from an EARLIER financial year than the run. The lookup still returns figures — it has to, or payroll would stop dead every April — so the run carries the warning instead.

Returns:

  • (Boolean)


110
111
112
# File 'lib/hr_lite/statutory_rate_card.rb', line 110

def self.stale_for?(period_month)
  FinancialYear.before?(effective_date_for(period_month), period_month)
end

.stored?Boolean

Deliberately rescued: the lookup is called from payroll, and a host mid-migration — gem upgraded, db:migrate not yet run — must fall back to the shipped figures rather than 500.

Returns:

  • (Boolean)


87
88
89
90
91
# File 'lib/hr_lite/statutory_rate_card.rb', line 87

def self.stored?
  StatutoryRateCardRecord.table_exists? && StatutoryRateCardRecord.exists?
rescue ActiveRecord::ActiveRecordError
  false
end

.unverified_for(period_month) ⇒ Object

A card exists for the run's year but nobody has signed it off. Ranked below staleness because wrong-year figures are the worse problem, but still worth saying: these numbers decide what lands in a bank account.



123
124
125
126
127
128
129
130
# File 'lib/hr_lite/statutory_rate_card.rb', line 123

def self.unverified_for(period_month)
  record = record_for(period_month)
  return nil if record.nil? || record.verified? || stale_for?(period_month)

  "The FY #{record.financial_year} statutory card has not been marked " \
  "verified. Check PF, ESI and the tax slabs with your accountant, then " \
  "record who confirmed them on the rate-card screen."
end

.warning_for(period_month) ⇒ Object

One sentence naming both the run's FY and the card's, or nil when they match. Rendered verbatim in the run's warnings list.



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/hr_lite/statutory_rate_card.rb', line 134

def self.warning_for(period_month)
  card_fy = FinancialYear.label(effective_date_for(period_month))
  run_fy  = FinancialYear.label(period_month)

  if predates_cards?(period_month)
    "Payroll for FY #{run_fy} is being computed on the FY #{card_fy} " \
    "statutory card — no card ships for a year that early. PF, ESI, PT " \
    "and TDS on this run are not the rates that applied. Verify with your CA."
  elsif stale_for?(period_month)
    "Payroll for FY #{run_fy} is being computed on the FY #{card_fy} " \
    "statutory card — no card ships for FY #{run_fy} yet. PF, ESI, PT and " \
    "TDS on this run use last year's rates. Add a CA-verified card for " \
    "FY #{run_fy} before publishing."
  end
end