A few months ago I wrote about modelling India's tax slabs without cliff-edge bugs. Gratuity looked easier after that. It's one line of maths:

const gratuity = (wages * 15 * years) / 26; 

That line is correct. The bugs live in its inputs. Below are the three edge cases I had to handle when building a gratuity calculator in Next.js, and the code that handles each one.

Edge case 1: the divisor depends on who the employer is

Indian gratuity law covers establishments with ten or more employees. For them, Explanation 3 to Section 53 of the Code on Social Security, 2020 says fifteen days' wages are calculated by dividing the monthly wage by 26 and multiplying by 15. The 26 goes back to a 1980 Supreme Court ruling that treated a month as 26 working days.

Smaller employers aren't covered. If they pay gratuity anyway, the income-tax rule works it out as half a month's average salary per completed year, which is 15/30.

So the divisor is a function of a boolean, and so is the way years are counted (more on that next):

const divisor = covered ? 26 : 30; const rawGratuity = (salary * 15 * serviceYears) / divisor; 

Multiply before you divide. 15 / 26 is a repeating decimal (0.576923...), and computing it first and multiplying later stacks floating-point error onto every result. Keeping the division last and rounding only at display time gives you rupee-accurate numbers.

Edge case 2: "more than six months" is > 6, not >= 6

Section 53(2) pays gratuity "for every completed year of service or part thereof in excess of six months". A covered employee with 7 years and 7 months gets paid for 8 years. One with 7 years and 6 months gets paid for 7.

The wording is "in excess of", so the comparison is strict:

// Covered employees round a part-year above six months up to a full year. // Employees outside the Act count only completed years. const serviceYears = covered ? years + (months > 6 ? 1 : 0) : years; 

Write >= 6 by mistake and, on ₹50,000 of monthly wages, that one character overpays an employee with exactly six extra months by ₹28,846.

The rounding rule also creates a useful UX feature. If someone is a few months short of the next threshold, the app can tell them how long to wait and what it's worth:

const nextMilestoneMonths = covered ? serviceYears * 12 + 7 - totalMonths : 0; const nextMilestoneGain = covered ? Math.min((salary * 15 * (serviceYears + 1)) / 26, CAP) - gratuity : 0; 

serviceYears * 12 + 7 is the first month count that rounds up to the next year. At 6 years 5 months, that's 79 months, so the user is 2 months away from an extra year of gratuity. Rounding never helps non-covered employees, so the nudge is switched off for them.

Edge case 3: the salary you plug in may not be "Basic"

This one changed with the labour codes that took effect on 21 November 2025. Gratuity is calculated on "wages", defined in Section 2(88) of the Code on Social Security. The definition excludes items like HRA, conveyance allowance, overtime, commission, bonus and the employer's PF contribution. Then a proviso adds a twist: if those excluded items are more than half of total remuneration, the excess is added back into wages.

A salary of ₹1,00,000 a month with Basic + DA of ₹35,000 and ₹65,000 in allowances has ₹15,000 above the 50% line. Wages for gratuity become ₹50,000, not ₹35,000.

In code, it's a small pure function:

/** * Wages for gratuity under Section 2(88), Code on Social Security, 2020. * excluded = sum of excluded components (HRA, conveyance, overtime, bonus...) * total = all remuneration for the month */ function gratuityWages({ basicDa, excluded, total }) { const addBack = Math.max(0, excluded - total / 2); return basicDa + addBack; } gratuityWages({ basicDa: 35000, excluded: 65000, total: 100000 }); // 50000 gratuityWages({ basicDa: 60000, excluded: 40000, total: 100000 }); // 60000 

My calculator currently asks the user for Basic + DA directly and shows a hint that the labour-code wage base may be higher. Asking people to classify every line of their payslip is a lot of friction, so I keep the input simple and explain the rule next to it. If you're building payroll software, though, this function belongs in your core logic, because it also feeds PF and other benefits.

The rest of the rules, compressed

These are smaller but still need code paths:

  • Eligibility. Permanent employees need 60 months of continuous service. Fixed-term employees need 12 months and get gratuity pro rata.
  • Ceiling. The statutory cap for private-sector employees is ₹20 lakh. Apply it after the formula: Math.min(rawGratuity, 2000000).
  • Near misses. Some High Courts have counted 4 years and 240 days as five years, but it isn't uniform. I flag users at 56+ months with a note instead of paying them out in the maths.

Test cases worth copying

Legal formulas deserve table-driven tests. In my app the logic sits inside a useMemo in the component, which makes it awkward to test, so the first step is pulling it into a pure calcGratuity() function. Then test it against cases like these:

Wages Service Covered Type Expected
50,000 7y 7m yes permanent ₹2,30,769
50,000 7y 6m yes permanent ₹2,01,923
50,000 7y 7m no permanent ₹1,75,000
40,000 1y 0m yes fixed-term ₹23,077
30,000 4y 11m yes permanent ₹0 (not eligible)
2,00,000 30y 0m yes permanent ₹20,00,000 (capped)
import { describe, it, expect } from 'vitest'; import { calcGratuity } from './gratuity'; const cases = [ [50000, 7, 7, true, 'permanent', 230769], [50000, 7, 6, true, 'permanent', 201923], [50000, 7, 7, false, 'permanent', 175000], [40000, 1, 0, true, 'fixedTerm', 23077], [30000, 4, 11, true, 'permanent', 0], [200000, 30, 0, true, 'permanent', 2000000], ]; describe('calcGratuity', () => { it.each(cases)('%i wages, %iy %im, covered=%s, %s', (salary, years, months, covered, empType, expected) => { expect(Math.round(calcGratuity({ salary, years, months, covered, empType }))).toBe(expected); }); }); 

The second row is the important one. If your > 6 ever becomes >= 6, it fails.

Formatting the output

Indian users read ₹2,30,769, not ₹230,769. Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }) handles the lakh grouping. I covered the gotchas of that in a separate post on Hashnode.

You can try all of these cases in the live gratuity calculator on MonuMoney. Flip the "covered" toggle on 7 years 7 months and watch ₹55,769 disappear.

If you've built anything around Indian payroll, I'd like to know which rule gave you the most trouble. For me it was the 50% clause, because it's the only one where the input itself is wrong before any maths happens.