DEV Community

Știuriuc Sorin-Marian
Știuriuc Sorin-Marian

Posted on

The fiscal edge cases Romanian payroll software gets wrong (and how I modeled them in TypeScript)

Three weeks ago I wrote about modeling Romania's salary taxes in TypeScript for salariile.ro. Since then, Romania's minimum wage changed (July 1st: 4,050 → 4,325 lei gross), and I shipped a payslip generator on top of the same fiscal engine: salariile.ro/fluturas-salariu.

Building it forced me to confront edge cases that, as far as I can tell from comparing outputs, even commercial payroll software gets wrong. Here are three of them.

1. The tax-free allowance survives bonuses (up to a ceiling)

Romania gives minimum-wage earners a fixed tax-free slice: 200 lei/month is exempt from all contributions and income tax (OUG 89/2025). The naive implementation:

const facilitate = (functieDeBaza && brut === SALARIU_MINIM) ? 200 : 0;
Enter fullscreen mode Exit fullscreen mode

That was literally my first version. It's wrong. The actual rule: the allowance applies when the base salary equals the minimum wage AND total gross income (excluding meal vouchers) stays under a ceiling — 4,600 lei since July. So minimum wage + a 200 lei bonus = allowance stays. Minimum wage + a 400 lei bonus = allowance gone, on the entire amount.

const salariuDeBaza = input.salariuDeBaza ? parseFloat(input.salariuDeBaza) : brut;
const facilitate =
  (functieDeBaza && salariuDeBaza === SALARIU_MINIM && brut <= PLAFON_FACILITATE)
    ? DEDUCERE_MINIM
    : 0;
Enter fullscreen mode Exit fullscreen mode

The difference is 80–100 lei/month in the employee's pocket. I've seen real payslips where software silently dropped the allowance the moment any bonus appeared.

2. Personal deduction rounding changed in 2023 — some software never noticed

The personal deduction (a progressive tax shield for lower salaries) used to be rounded up to the nearest 10 lei. Since 2023, the Fiscal Code says you apply the exact computed amount, rounded to the leu.

For a 4,425 lei gross salary: 19% × 4,325 = 821.75 → 822 lei. A well-known Romanian calculator still shows 800 for this case — the old rounding, cascading into a wrong income tax.

How do I know mine is right? I validate against ANAF's own D112 declaration validator (DUKIntegrator) — the same software employers' declarations run through. If my numbers diverge from the official validator, my tests fail.

3. There is no such thing as a 168-hour month

Hourly rate = base salary ÷ monthly working hours. The lazy constant is 168 (21 days × 8h). But Romanian months have 19–23 working days once you subtract weekends and legal holidays. July 2026 has 23 → 184 hours → 23.51 lei/hour at minimum wage, not 25.74.

I generate the working-hours calendar from a single source of truth (a map of legal holidays, shared between the public holidays page and the payslip PDF), so overtime pay is computed on the real month, not an average.

export function zileLucratoareLuna(an: number, luna0: number): number {
  const zileInLuna = new Date(Date.UTC(an, luna0 + 1, 0)).getUTCDate();
  let lucratoare = 0;
  for (let d = 1; d <= zileInLuna; d++) {
    const dow = new Date(Date.UTC(an, luna0, d)).getUTCDay();
    const weekend = dow === 0 || dow === 6;
    const sarbatoare = an === 2026 && Boolean(SARBATORI_LEGALE_2026[`${luna0 + 1}-${d}`]);
    if (!weekend && !sarbatoare) lucratoare++;
  }
  return lucratoare;
}
Enter fullscreen mode Exit fullscreen mode

The meta-lesson

Fiscal law is a spec written in prose, full of exceptions that only surface when real people hit them. The only defense I've found: one pure calculation module as the single source of truth, tests pinned to the official validator's output, and treating every divergence from another calculator as a research task — sometimes they're wrong, sometimes I am.

The whole thing is open source: github.com/xsagul/salariile-ro. If you're Romanian and want to check your own payslip against the law, the generator is free, no account: salariile.ro/fluturas-salariu.

If you've built payroll or tax tools for other countries — do you also validate against the government's own software, or is that a Romanian luxury?

Top comments (0)