<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Știuriuc Sorin-Marian</title>
    <description>The latest articles on DEV Community by Știuriuc Sorin-Marian (@sorin_stiuriuc).</description>
    <link>https://dev.to/sorin_stiuriuc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3942973%2F039666fd-da2a-4038-a866-9d0924925532.jpg</url>
      <title>DEV Community: Știuriuc Sorin-Marian</title>
      <link>https://dev.to/sorin_stiuriuc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sorin_stiuriuc"/>
    <language>en</language>
    <item>
      <title>The fiscal edge cases Romanian payroll software gets wrong (and how I modeled them in TypeScript)</title>
      <dc:creator>Știuriuc Sorin-Marian</dc:creator>
      <pubDate>Thu, 02 Jul 2026 18:24:45 +0000</pubDate>
      <link>https://dev.to/sorin_stiuriuc/the-fiscal-edge-cases-romanian-payroll-software-gets-wrong-and-how-i-modeled-them-in-typescript-152</link>
      <guid>https://dev.to/sorin_stiuriuc/the-fiscal-edge-cases-romanian-payroll-software-gets-wrong-and-how-i-modeled-them-in-typescript-152</guid>
      <description>&lt;p&gt;Three weeks ago I wrote about &lt;a href="https://dev.to/sorin_stiuriuc"&gt;modeling Romania's salary taxes in TypeScript&lt;/a&gt; for &lt;a href="https://salariile.ro" rel="noopener noreferrer"&gt;salariile.ro&lt;/a&gt;. 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: &lt;a href="https://salariile.ro/fluturas-salariu" rel="noopener noreferrer"&gt;salariile.ro/fluturas-salariu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The tax-free allowance survives bonuses (up to a ceiling)
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;facilitate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;functieDeBaza&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;brut&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;SALARIU_MINIM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was literally my first version. It's wrong. The actual rule: the allowance applies when the &lt;em&gt;base&lt;/em&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;salariuDeBaza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;salariuDeBaza&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;salariuDeBaza&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;brut&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;facilitate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;functieDeBaza&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;salariuDeBaza&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;SALARIU_MINIM&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;brut&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;PLAFON_FACILITATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;DEDUCERE_MINIM&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Personal deduction rounding changed in 2023 — some software never noticed
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. There is no such thing as a 168-hour month
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I generate the working-hours calendar from a single source of truth (a map of legal holidays, shared between the &lt;a href="https://salariile.ro/zile-libere-2026" rel="noopener noreferrer"&gt;public holidays page&lt;/a&gt; and the payslip PDF), so overtime pay is computed on the real month, not an average.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;zileLucratoareLuna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;an&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;luna0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zileInLuna&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;an&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;luna0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getUTCDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lucratoare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;zileInLuna&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;an&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;luna0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getUTCDay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weekend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dow&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;dow&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sarbatoare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;an&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SARBATORI_LEGALE_2026&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;luna0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;weekend&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sarbatoare&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;lucratoare&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lucratoare&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The whole thing is open source: &lt;a href="https://github.com/xsagul/salariile-ro" rel="noopener noreferrer"&gt;github.com/xsagul/salariile-ro&lt;/a&gt;. If you're Romanian and want to check your own payslip against the law, the generator is free, no account: &lt;a href="https://salariile.ro/fluturas-salariu" rel="noopener noreferrer"&gt;salariile.ro/fluturas-salariu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Modeling Romania's salary taxes in TypeScript: lessons from building salariile.ro</title>
      <dc:creator>Știuriuc Sorin-Marian</dc:creator>
      <pubDate>Fri, 12 Jun 2026 18:36:15 +0000</pubDate>
      <link>https://dev.to/sorin_stiuriuc/modeling-romanias-salary-taxes-in-typescript-lessons-from-building-salariilero-20o</link>
      <guid>https://dev.to/sorin_stiuriuc/modeling-romanias-salary-taxes-in-typescript-lessons-from-building-salariilero-20o</guid>
      <description>&lt;p&gt;I spent the last few weeks building &lt;a href="https://salariile.ro" rel="noopener noreferrer"&gt;salariile.ro&lt;/a&gt;, a salary calculator for Romania. It sounds like a weekend project: take the gross salary, subtract three percentages, show the net. It was not. Here is what made it interesting, and what I would tell anyone who needs to model payroll or tax rules in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The domain is the hard part
&lt;/h2&gt;

&lt;p&gt;Romanian salary math in 2026 looks simple on paper: 25% pension contribution (CAS), 10% health insurance (CASS), 10% income tax after deductions, plus 2.25% paid by the employer on top (CAM). The complexity hides in the details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The personal deduction is not a constant. It is a curve that depends on income and the number of dependents, and it phases out completely above a threshold.&lt;/li&gt;
&lt;li&gt;Meal vouchers are taxed differently from cash. CASS and income tax apply to their value, but the tax money is withheld from your cash salary while the full nominal value lands on the card. Many calculators get this subtly wrong, and people then wonder why the payslip does not match the estimate.&lt;/li&gt;
&lt;li&gt;Sector rules (construction, agriculture) and the minimum wage facility change the formula depending on contract details.&lt;/li&gt;
&lt;li&gt;The rules change constantly. The minimum wage changes again on July 1, 2026, and several thresholds are indexed to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My main takeaway: encode rules as data, not as ifs. Every fiscal period is a config object with rates, thresholds, deduction tables and links to the legal source. The calculation engine is a pure function from (config, input) to a payslip breakdown. When the July 2026 rules land, that will be a new config entry plus a set of tests, not a refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test against official examples
&lt;/h2&gt;

&lt;p&gt;The Romanian tax authority publishes norms for Declaration 112 with worked examples. Every rule I implement gets a test case traced back to an official source, and where two sources disagree, a comment links the exact article of law I followed. For a YMYL topic this is the only thing that keeps the project honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The web part: boring on purpose
&lt;/h2&gt;

&lt;p&gt;Next.js 16 App Router, React 19, TypeScript, deployed on Vercel. Server Components everywhere, with the calculator as the only real client component. No cookies, no ads, no tracking scripts. For search visibility the unglamorous things mattered most: correct JSON-LD (Person, FAQPage), a sitemap that actually updates, llms.txt for AI crawlers, and pages that each answer one question instead of one page trying to answer everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;While researching the niche I found that the sites ranking top 3 for "calculator salariu" include one with almost zero backlink authority and another still showing rules that expired in 2024. In this niche, search engines seem to reward being current and precise more than raw link mass. Encouraging, if you are building something accurate.&lt;/p&gt;

&lt;p&gt;The calculator is live at &lt;a href="https://salariile.ro" rel="noopener noreferrer"&gt;salariile.ro&lt;/a&gt; and I am happy to answer questions about modeling tax rules in TypeScript. It is a surprisingly fun domain once you accept the chaos.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
