<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Engage HRMS Payroll Notes]]></title><description><![CDATA[Engage HRMS Payroll Notes]]></description><link>https://engagehrms.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 07:33:51 GMT</lastBuildDate><atom:link href="https://engagehrms.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The ESIC monthly contribution file: six columns, and the format bugs that get it rejected]]></title><description><![CDATA[The portal wants a .xls of days and wages, not a contribution report. A last working day goes in only for a short list of reasons.]]></description><link>https://engagehrms.hashnode.dev/the-esic-monthly-contribution-file-six-columns-and-the-format-bugs-that-get-it-rejected</link><guid isPermaLink="true">https://engagehrms.hashnode.dev/the-esic-monthly-contribution-file-six-columns-and-the-format-bugs-that-get-it-rejected</guid><category><![CDATA[Payroll]]></category><category><![CDATA[esi]]></category><category><![CDATA[india]]></category><category><![CDATA[compliance ]]></category><category><![CDATA[engineering]]></category><dc:creator><![CDATA[Engage HRMS]]></dc:creator><pubDate>Wed, 11 Nov 2026 18:17:07 GMT</pubDate><content:encoded><![CDATA[<p>Most ESI filing trouble we see is not the contribution maths. It is the Excel file. Someone saves it as .xlsx, leaves a date as an Excel date value, or puts a last working day on a person who was only on leave. The portal rejects the upload, or accepts it and drops the person from the next month's list.</p>
<p>EngageHRMS (hrengage.ai) is an Indian HR and payroll software company in Gurgaon. I work there.</p>
<ul>
<li>ESI in EngageHRMS is calculated from attendance in the same payroll run.</li>
<li>That run already has the two figures the ESIC file is supposed to carry: the days for which wages were paid or payable, and the total monthly wages.</li>
<li>We use those same figures on the ESI line of the payslip. The portal, not the spreadsheet, works out the contribution.</li>
</ul>
<p>Zoho Payroll, greytHR and Keka have to get a file through the same portal. The rates are the easy part. The format is what usually fails.</p>
<h2>What the file actually is</h2>
<p>On the ESIC employer portal, monthly contribution and challan generation is a two-step process. First, make sure every insured person (IP) is mapped. Then go to File Monthly Contributions, pick the month and year, and file.</p>
<p>There are two ways to file: an Excel upload, or online entry. This note is about the Excel route. Download the sample MC Excel template from that page, and take the column headings and reason codes from that file. The reason-code column has a link to the official list.</p>
<p>The official help file, linked from the employer help page, shows a six-column sheet in this order:</p>
<ol>
<li>IP Number (10 digits)</li>
<li>IP Name (only alphabets and space)</li>
<li>No of Days for which wages paid/payable during the month</li>
<li>Total Monthly Wages</li>
<li>Reason Code for Zero working days (numeric only; provide 0 for all other reasons)</li>
<li>Last Working Day (dd-mm-yyyy or dd/mm/yyyy)</li>
</ol>
<p>There is no contribution column. The help file is clear: IP and employer contributions are calculated by the system. ESIC's FAQ says the same thing: you file the days and the wages, and the contribution is worked out from those. If your export has 0.75% and 3.25% columns, you are building the wrong file.</p>
<p>Save it as .xls (Excel 97-2003). A modern .xlsx is not what the portal asks for.</p>
<h2>Days, wages, and a last working day</h2>
<p>Days must be a whole number. If you have a fraction, the help file says to round it up to the next higher integer. That is not "round to nearest". Pull paid or payable days from the same attendance that built the payslip, then apply this rounding once.</p>
<p>Every IP currently mapped to the employer on the portal must be in the sheet, with the details entered correctly. A missing mapped person is enough for the upload to fail.</p>
<p>The reason code is only a number. The template says to put 0 for all other reasons, meaning anyone who is not a zero-working-days case. The last working day is narrower still. The help file allows a date only when the employee has left service, retired, gone out of coverage, expired, moved to a non-implemented area, or been retrenched. For every other reason, the last working day must be left blank.</p>
<p>The same page says that once 0 wages are given, the IP is removed from the employer's record and will not appear in later months. Read that next to the list above. Those six reasons are exits. The instructions also tell you to leave the date blank for other reasons, which only makes sense if a temporary zero-wage month is not the same as an exit. If you are not sure whether a row will drop the person next month, check the upload before you submit.</p>
<p>Do not invent a last working day just to fill the cell. A date on a leave row, or a blank date on a leaver, is how people disappear from the file, or stay in it after they have gone.</p>
<h2>Why the portal rejects a file that looks fine on screen</h2>
<p>These are the format rules in the official help file, and they are stricter than they look.</p>
<p>All six columns, including the date column, must be stored as Text. Excel likes to turn a date you typed as 19/08/2026 into a date value, and it may display that value in your computer's regional format. It looks right. It is not text, which is what the portal wants. The help file's fix is Data, then Text to Columns, for each column from A to F, with the last step set to Text. It also allows a round-trip through Notepad after the column is set to Text.</p>
<p>Dates must be padded. 02/05/2010 or 02-05-2010 is accepted. 2/5/2010 or 2-May-2010 is not.</p>
<p>The IP name may contain letters and spaces only. A full stop in "S. Kumar", or a digit in the name field, is enough to make a row fail.</p>
<p>Turn off the browser's pop-up blocker before you upload. The help file repeats this, because the portal uses pop-ups for the upload and the challan.</p>
<p>If an upload keeps failing after you have fixed the data, download a fresh template and paste only the data area, starting at cell A2, into that new file. Do not paste a whole workbook on top of the headings.</p>
<pre><code class="language-python">def check_mc_row(ip, name, days, wages, reason, lwd, mapped_ips):
    assert ip.isdigit() and len(ip) == 10
    assert all(ch.isalpha() or ch == " " for ch in name)
    assert isinstance(days, int) and days &gt;= 0
    if str(reason) == "0":
        assert lwd in ("", None)
    if lwd:
        assert len(lwd) == 10 and lwd[2] in "/-" and lwd[5] in "/-"
    assert ip in mapped_ips
</code></pre>
<p>The function is a pre-upload check. It does not invent reason codes or compute the contribution. On online entry, the portal rounds the employee share up to the next rupee.</p>
<h2>After the file goes in</h2>
<p>Once the monthly contribution is submitted, it cannot be changed. The only route is a supplementary contribution, and only after the monthly filing is already in.</p>
<p>A supplementary filing can be made more than once. It can only increase the amount due. It cannot reduce what you have already filed. If the days or wages for an IP were short, enter the corrected figures and the portal adds the extra contribution. If you missed an IP, register or insert that person first, then file the supplementary. If a challan already exists, the extra amount becomes a second challan. It does not rewrite the first one.</p>
<p>ESIC's published FAQ says the contribution is to be paid within 15 days of the last day of the calendar month in which it falls due. File early enough that a rejected upload still leaves time to pay.</p>
<h2>Bugs that show up in naive exports</h2>
<ol>
<li>The file is saved as .xlsx, or as a contribution report with rate columns, instead of the six-column .xls template.</li>
<li>Days or dates are stored as Excel numbers, not as text.</li>
<li>A date is written as 2/5/2016 or 2-May-2016, and the portal rejects the row.</li>
<li>A last working day is filled in for a person on leave, or left blank for someone who has actually left.</li>
<li>A mapped IP is missing from the sheet, so the upload fails for the whole file.</li>
<li>The name field has a full stop, a hyphen or a digit.</li>
<li>A short-days month is rounded to the nearest day, not up to the next whole number.</li>
</ol>
<p>Most of these go away if one function builds the six text columns from the payroll run that already produced the payslip, and a second function checks the file against the mapped IP list before anyone clicks Upload.</p>
<p>More on how we handle ESI in payroll is at <a href="https://hrengage.ai">https://hrengage.ai</a>. Check the live template and help file before you change a live setup.</p>
<h2>Disclaimer</h2>
<p>This is a working summary of the portal file, not legal advice. Use the sample template on the employer portal, and the help file linked there, for reason codes and for any change in the format.</p>
<h2>Sources</h2>
<ul>
<li>Employees' State Insurance Corporation, User Manual for Filing the Monthly Contribution and Payment of dues, linked from the employer help page as "Help for Monthly contribution and Challan (Updated)": <a href="https://portal.esic.gov.in/InsuranceGlobalWebV4/ESICInsurancePortal/Documents/MC_Presentation.pdf">https://portal.esic.gov.in/InsuranceGlobalWebV4/ESICInsurancePortal/Documents/MC_Presentation.pdf</a> (column headings also taken from the template screenshot in that file)</li>
<li>Employer help files index: <a href="https://portal.esic.gov.in/InsuranceGlobalWebV4/ESICInsurancePortal/HelpFiles.aspx">https://portal.esic.gov.in/InsuranceGlobalWebV4/ESICInsurancePortal/HelpFiles.aspx</a></li>
<li>Employees' State Insurance Corporation, FAQ, question 20 (time limit for contribution, within 15 days of the last day of the calendar month) and question 21 (file days and wages; the system ascertains the contribution): <a href="https://esic.gov.in/attachments/files/faq.pdf">https://esic.gov.in/attachments/files/faq.pdf</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[NCP days in the EPF ECR: how field 10 works and the payroll bugs that miscount it]]></title><description><![CDATA[A whole-number field that most payroll code fills from the wrong table]]></description><link>https://engagehrms.hashnode.dev/ncp-days-epf-ecr-field-10</link><guid isPermaLink="true">https://engagehrms.hashnode.dev/ncp-days-epf-ecr-field-10</guid><category><![CDATA[Payroll]]></category><category><![CDATA[EPF]]></category><category><![CDATA[india]]></category><category><![CDATA[compliance ]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Engage HRMS]]></dc:creator><pubDate>Tue, 10 Nov 2026 08:05:26 GMT</pubDate><content:encoded><![CDATA[<p>Field 10 of the ECR upload is a whole-number day count. Most of the wrong lines we see come from code that filled it from the attendance table or the leave register instead of from paid days. This note is for whoever builds the return file or signs it off.</p>
<h2>The field itself</h2>
<p>An ECR 2.0 file is plain text with one member per line and 11 fields separated by <code>#~#</code>:</p>
<pre><code class="language-plaintext">UAN#~#Name#~#Gross#~#EPF_wages#~#EPS_wages#~#EDLI_wages#~#EE_share#~#EPS_share#~#ER_share#~#NCP_days#~#Refund
</code></pre>
<p>NCP means non-contributory period. An NCP day is a day in the wage month for which the member earned no wages, so no contribution arises for it.</p>
<p>EPFO's ECR 2.0 introduction lists two rules for field 10. Half-day NCP is not permitted, so the value is a whole number. And if the wages declared are 0 because the member earned nothing, NCP must equal the number of days in the month. Nothing in that document says field 10 is checked against the wage fields on the same line. Assume the consistency between days and wages is your job, not the portal's.</p>
<p>The working formula:</p>
<pre><code class="language-plaintext">ncp_days = days_in_wage_month - paid_days
</code></pre>
<p>October gives you 31 days, February 28 (29 in a leap year). The divisor is calendar days. It is not 26 and it is not working days. Because half days are rejected, an unpaid half day needs a written rounding rule before anyone builds the file.</p>
<h2>What counts as a paid day</h2>
<p>Paid leave of any type carries wages, so it is never NCP. Weekly offs and paid holidays inside the employment period are paid days too.</p>
<p>Two cases need a written policy rather than a code default.</p>
<p>A weekly off sitting inside a run of unpaid absence (the "sandwich" case). If your leave policy pays that Sunday, it is not NCP. If the policy treats it as unpaid, it is. Decide once, so the answer does not depend on who runs the month.</p>
<p>Maternity leave depends on who pays. Where the employer pays the benefit, those days carry wages. Where the employee is under ESI and draws the benefit from ESIC, the employer pays no wages for those days. Check how your setup handles each route instead of hard-coding one answer.</p>
<h2>A worked line below the ceiling</h2>
<p>Basic plus DA of Rs 12,000, a 30-day month, 3 unpaid days:</p>
<pre><code class="language-plaintext">paid_days = 27
ncp_days  = 3
epf_wages = 12000 * 27 / 30  = 10800
ee_share  = 12% of 10800     = 1296
eps_share = 8.33% of 10800   = 900    (899.64, rounded)
er_share  = 1296 - 900       = 396
</code></pre>
<h2>The ceiling moved on 17 September 2026</h2>
<p>S.O. 5109(E), published in the Gazette on 17 September 2026 under the Code on Social Security, 2020, set the wage ceiling for Chapter III (EPF, EPS and EDLI) at Rs 25,000 a month from the date of publication. The old figure was Rs 15,000 and had not changed since September 2014. At 8.33%, the maximum monthly EPS share goes from Rs 1,250 to Rs 2,083.</p>
<p>September 2026 is a split month. EPFO's FAQs on the change compute it in two parts: 1 to 16 September at Rs 15,000, 17 to 30 September at Rs 25,000, reported in a single ECR. The due date was first 15 October 2026, and an EPFO circular of 13 October 2026 moved it to 31 October 2026 for September only. For an existing member on Rs 20,000, the FAQ illustration works out to Rs 15,000 x 16/30 = Rs 8,000 plus Rs 20,000 x 14/30 = Rs 9,333.33, so EPF wages of Rs 17,333.33 for the month. A September line showing a flat 15,000 or a flat 20,000 for that person is off in one direction or the other.</p>
<h2>Where NCP days meet the ceiling</h2>
<p>Take an employee on Rs 30,000 with 3 unpaid days in a 30-day month. Earned basic is Rs 27,000. Payroll code usually does one of two things:</p>
<pre><code class="language-plaintext"># A: cap the earned wage
epf_wages = min(30000 * 27/30, 25000)  # 25000

# B: prorate the ceiling by paid days
epf_wages = min(30000, 25000) * 27/30  # 22500
</code></pre>
<p>We run reading B. It keeps EPF wages and field 10 telling the same story, and it uses the same day-proportion arithmetic EPFO applied to the September split. We could not find EPFO guidance that settles it for an ordinary month, though, and plenty of payrolls run reading A. Choose one on purpose, store it as a single config value, and note it in the compliance file. The failure to avoid is A for some employees and B for others because two people wrote two code paths.</p>
<h2>Joiners and leavers</h2>
<p>Someone who joins on the 18th of a 30-day month is paid for 13 days. On the formula reading, field 10 is 17. On the date-based reading it is 0, because days before joining are not absence and the joining date already bounds the service. Both are used in practice, and the ECR material does not choose between them.</p>
<p>Use the same reading for joiners and leavers. A codebase that reports 17 for joiners and 0 for leavers is the version that causes trouble. For every leaver, mark the date of exit in the month they leave. Without an exit date, the member generally cannot claim final settlement for that employment.</p>
<h2>Why a wrong count surfaces years later</h2>
<p>Not at the challan. At the pension claim.</p>
<p>The Employees' Pension Scheme, 2026 kept the old formula: monthly pension = pensionable salary x pensionable service / 70, with pensionable salary averaged over the 60 months before exit and 10 years of service needed for a monthly pension. The scheme defines non-contributory service as service for which no pension contribution was received or receivable. A paid Sunday wrongly filed as NCP shows up on the member's record as a day without a contribution.</p>
<p>The salary average is protected from this. EPFO's Pension Manual divides total wages in the 60-month span by the actual days paid and multiplies by 30, so NCP days do not pull the average down. The service count is where the error stays.</p>
<p>The EDLI Scheme, 2026 (G.S.R. 526(E), 29 June 2026) sets the death benefit at a minimum of Rs 2.5 lakh and a maximum of Rs 7 lakh. The minimum depends on 12 months of continuous service, and a gap of up to 60 days between two spells of employment is ignored. That rule is about gaps between jobs, so the joining and exit dates you file matter there as much as field 10 does. EPFO has also said the Rs 7 lakh cap stays even though the new ceiling would push the formula higher.</p>
<p>Contributions and the return are due within 15 days of the close of the month, so by the 15th.</p>
<h2>Bugs that show up in naive code</h2>
<ol>
<li>Field 10 read from the leave register, so paid leave becomes NCP and the day count disagrees with the wages on the same line.</li>
<li>A 26-day divisor for NCP and calendar days for wages, or the reverse.</li>
<li>Ceiling readings A and B mixed across employees.</li>
<li>Different joiner and leaver conventions in different modules.</li>
<li>No check that paid days plus NCP days equals days in the month.</li>
<li>Unpaid half days rounded by whichever operator runs the file.</li>
<li>September 2026 lines built with one flat ceiling instead of the 16/14 split.</li>
</ol>
<p>Most of these go away if one function owns paid days and everything else derives from it:</p>
<pre><code class="language-python">def ecr_line(emp, month, ceiling=25000, mode="prorate"):
    days = month.calendar_days
    paid = emp.paid_days(month)        # single source of truth
    ncp = days - paid
    assert isinstance(ncp, int) and 0 &lt;= ncp &lt;= days

    full = emp.pf_basic_da             # monthly rate, not earned
    if mode == "prorate":              # reading B
        epf = min(full, ceiling) * paid / days
    else:                              # reading A
        epf = min(full * paid / days, ceiling)

    epf = round(epf)
    ee = round(epf * 0.12)
    eps = round(epf * 0.0833)          # assumes an EPS member under 58
    return epf, ee, eps, ee - eps, ncp
</code></pre>
<p>The function skips higher-wage contributions, members over 58 and non-EPS members. Each needs its own branch.</p>
<h2>Checks before you upload</h2>
<ol>
<li>For every line, paid days plus NCP days equals days in the wage month.</li>
<li>No line has full-month EPF wages with NCP above 0, unless the documented ceiling reading explains it.</li>
<li>No line has reduced wages with NCP of 0, unless the date-based joiner or leaver reading applies and the dates are correct.</li>
<li>Paid leave, weekly offs and paid holidays are absent from the NCP total.</li>
<li>Where the ceiling bites, EPF wages match your chosen reading for every employee.</li>
<li>Every leaver in the month has a date of exit.</li>
<li>Every NCP value is a whole number between 0 and the days in the month.</li>
</ol>
<p>Run the list against the same payroll run that produced the ECR, before upload. The portal will not do it for you.</p>
<p>The longer EngageHRMS guide, with claim-side examples, is at <a href="https://hrengage.ai/blog/ncp-days-in-pf">https://hrengage.ai/blog/ncp-days-in-pf</a>. Check the live EPFO position before you change your setup.</p>
]]></content:encoded></item><item><title><![CDATA[The ESI contribution period rule that breaks naive payroll code]]></title><description><![CDATA[A common bug in Indian ESI handling comes from one wrong assumption. Developers check the employee's gross wage every month, compare it with Rs 21,000, and…]]></description><link>https://engagehrms.hashnode.dev/the-esi-contribution-period-rule-that-breaks-naive-payroll-code</link><guid isPermaLink="true">https://engagehrms.hashnode.dev/the-esi-contribution-period-rule-that-breaks-naive-payroll-code</guid><category><![CDATA[Payroll]]></category><category><![CDATA[esi]]></category><category><![CDATA[india]]></category><category><![CDATA[compliance ]]></category><category><![CDATA[engineering]]></category><dc:creator><![CDATA[Engage HRMS]]></dc:creator><pubDate>Thu, 05 Nov 2026 09:37:00 GMT</pubDate><content:encoded><![CDATA[<p>A common bug in Indian ESI handling comes from one wrong assumption. Developers check the employee's gross wage every month, compare it with Rs 21,000, and switch ESI on or off. It looks correct. It isn't.</p>
<h2>How ESI coverage actually works</h2>
<p>ESI wages are counted in two contribution periods a year. The first runs from 1 April to 30 September and the second from 1 October to 31 March. If an employee is covered at the start of a contribution period, they stay covered for that whole period, even if a raise pushes their monthly wages above Rs 21,000 halfway through.</p>
<p>So an employee earning Rs 20,500 on 1 October who gets a raise to Rs 23,000 in December still pays ESI for December, January, February and March. Coverage is decided again on 1 April.</p>
<p>The monthly check gets this wrong in both directions. It stops deductions too early after a raise, and it can start them in the middle of a period for someone who was never covered at its start.</p>
<h2>What the data model needs</h2>
<p>A month's wage isn't enough to decide ESI. The engine needs three things for each employee:</p>
<ol>
<li>The contribution period the payroll month falls in.</li>
<li>Whether the employee was covered on the first day of that period, or joined during it with wages inside the limit.</li>
<li>The current month's ESI wages, for calculating the amount itself.</li>
</ol>
<p>In practice that means storing a coverage flag per employee per contribution period, set once when the period opens or when the employee joins, and never recalculated from a later month's salary.</p>
<pre><code class="language-python">def contribution_period(year, month):
    # April to September is the first period, October to March the second
    if 4 &lt;= month &lt;= 9:
        return (year, 1)
    start_year = year if month &gt;= 10 else year - 1
    return (start_year, 2)

def esi_applies(employee, year, month):
    period = contribution_period(year, month)
    return employee.esi_covered.get(period, False)
</code></pre>
<p>The coverage flag gets written in one place: the job that opens a new period, or the joining flow. Salary revisions don't touch it.</p>
<h2>The amounts</h2>
<p>Once coverage is settled, the maths is simple. The employee contributes 0.75% of ESI wages and the employer 3.25%. Employees whose average daily wage is Rs 176 or less don't pay the employee share, though the employer share still applies.</p>
<p>Contributions are rounded up to the next rupee, not to the nearest one. A plain <code>round()</code> call gives a different answer about half the time, and that gap shows up when the challan total doesn't match the sum of the payslips.</p>
<pre><code class="language-python">import math

def esi_amounts(esi_wages, avg_daily_wage):
    employer = math.ceil(esi_wages * 0.0325)
    employee = 0 if avg_daily_wage &lt;= 176 else math.ceil(esi_wages * 0.0075)
    return employee, employer
</code></pre>
<h2>Edge cases worth testing</h2>
<p>These cases are worth a test each:</p>
<ol>
<li>A raise in the middle of a period. Deductions should continue until the period ends.</li>
<li>An employee who joins in November above Rs 21,000. They aren't covered, and the October rule doesn't bring them in.</li>
<li>Arrears paid in a later month. The arrear months matter, so tie the coverage check to the month the wages were earned, not the month they were paid.</li>
<li>Rehires. Treat the new joining date as a fresh coverage decision inside the current period.</li>
<li>Months with a few working days. The daily wage test can flip for low earners, so compute it from the actual days paid.</li>
</ol>
<h2>Why it matters</h2>
<p>ESI mistakes are quiet. Payslips look fine, the employee doesn't notice a missing deduction of a little over a hundred rupees, and the problem only surfaces during an inspection or when someone needs a medical benefit and their contribution record has gaps. Getting the period logic right once saves a lot of reconciliation later.</p>
<p>If your engine still decides ESI from a single month's salary, that's the first thing we'd look at.</p>
<p>Rates and the Rs 21,000 limit are as notified by ESIC. Check the current ESIC notifications before relying on them for filing.</p>
<p>Written by the EngageHRMS payroll team.</p>
]]></content:encoded></item><item><title><![CDATA[Why TDS stays high after a bonus: how Indian payroll engines recalculate salary tax]]></title><description><![CDATA[A worked example of annual TDS projection, the residual spread over the remaining months, and the edge cases that break naive payroll code.]]></description><link>https://engagehrms.hashnode.dev/why-tds-stays-high-after-a-bonus-how-indian-payroll-engines-recalculate-salary-tax</link><guid isPermaLink="true">https://engagehrms.hashnode.dev/why-tds-stays-high-after-a-bonus-how-indian-payroll-engines-recalculate-salary-tax</guid><category><![CDATA[Payroll]]></category><category><![CDATA[TDS]]></category><category><![CDATA[india]]></category><category><![CDATA[compliance ]]></category><category><![CDATA[engineering]]></category><dc:creator><![CDATA[Engage HRMS]]></dc:creator><pubDate>Tue, 03 Nov 2026 16:41:12 GMT</pubDate><content:encoded><![CDATA[<p>After a large bonus, payroll gets the same ticket every year: "My bonus was paid last month. Why is TDS still high this month?" The engine is almost always right. Indian payroll systems project tax for the whole year, subtract what has already been deducted, and spread the balance over the months that are left. A bonus raises the projection, so the deduction stays higher for the rest of the year, bonus or no bonus. The example below walks through the numbers so engineers and payroll ops can debug these tickets from the formula itself.</p>
<h3>How payroll engines calculate monthly TDS on salary</h3>
<p>Most Indian payroll systems use the same three steps every month. First, estimate the employee's tax for the full tax year from salary paid so far, salary still expected, and any one-time income. Second, subtract the TDS already deducted this year. Third, divide what is left by the number of payroll months remaining, counting the current month.</p>
<p>A bonus changes step one. The annual estimate goes up, so the residual goes up, and because the residual is spread over the remaining months, the monthly deduction stays higher long after the bonus payslip. The form names changed from tax year 2026-27 (Form 138 for the quarterly statement, Form 130 for the annual certificate), but the projection works the same way.</p>
<h3>Inputs the engine needs</h3>
<ul>
<li>The employee's tax regime (old or new), and whether your policy lets them change it mid-year</li>
<li>Monthly taxable salary components after the exemptions you support, such as HRA where claimed</li>
<li>Declared deductions you accept mid-year (investments, housing loan interest), each with an effective date</li>
<li>Bonus, arrears, leave encashment and other one-time taxable amounts, with the month each one is paid</li>
<li>TDS already deducted from April up to last month</li>
<li>The number of payroll months left in the tax year, counting the current run</li>
</ul>
<p>If any of these is stale, the jump looks mysterious even when the formula is correct.</p>
<h3>Worked example: a bonus that crosses the rebate line</h3>
<p>These numbers illustrate the method and are not advice for a real employee.</p>
<ul>
<li>Tax year 2026-27 (April 2026 to March 2027), new regime</li>
<li>Gross salary: Rs 1,00,000 a month, Rs 12,00,000 for the year</li>
<li>Standard deduction: Rs 75,000</li>
<li>Taxable income before the bonus: Rs 11,25,000</li>
</ul>
<p>At Rs 11,25,000 the rebate brings tax down to zero, because taxable income is under Rs 12,00,000. Monthly TDS from April to September is therefore Rs 0.</p>
<p>In October the employee gets a taxable bonus of Rs 3,00,000.</p>
<ul>
<li>New gross for the year: Rs 15,00,000</li>
<li>Taxable income: Rs 15,00,000 minus Rs 75,000 = Rs 14,25,000</li>
<li>The rebate no longer applies, so tax runs through the slabs: nil on the first Rs 4,00,000, Rs 20,000 at 5% on the next Rs 4,00,000, Rs 40,000 at 10% on the next Rs 4,00,000, and Rs 33,750 at 15% on the last Rs 2,25,000</li>
<li>Tax before cess: Rs 93,750. With 4% cess: Rs 97,500</li>
<li>Already deducted: Rs 0</li>
<li>Months left, counting October: 6</li>
<li>New monthly TDS: Rs 97,500 / 6 = Rs 16,250</li>
</ul>
<p>So an employee who paid no TDS for six months now loses Rs 16,250 every month until March, and that includes November, when no bonus is paid. That is the ticket. The engine is collecting the year's tax on income that crossed the rebate line. It is not taxing October's bonus twice.</p>
<p>Some payroll teams deduct more of the extra tax in the bonus month itself, so the later months stay closer to normal. Both approaches collect the same total by March. Pick one, write it down, and make the payslip show which one you use.</p>
<p>If the employee later submits a valid deduction, the projection drops and so does the monthly cut. If the engine leaves the bonus out of the projection, November looks fine, and the shortfall shows up in March or in the employee's return. It is better to take the uncomfortable deduction every month than to spring a large one at year end.</p>
<h3>Edge cases that break naive TDS code</h3>
<p>Mid-year joiners. A joiner in August has eight projection months, not twelve. Spreading the annual estimate over twelve months under-deducts early and over-corrects later. Annualising their salary as if they had worked from April gives the opposite error.</p>
<p>Mid-year leavers. The full and final settlement should bring projected tax in line with actual taxable income for the months worked. If the engine still spreads a residual over months the person will never be paid for, someone ends up chasing a recovery or a refund.</p>
<p>Regime changes. If your policy allows a switch mid-year, recompute the annual estimate under the new regime. Applying old-regime slabs to new-regime income is a common bug.</p>
<p>Previous employment. When a new employee declares income earned and TDS deducted by a previous employer, both belong in the projection. Skip them and March brings a surprise.</p>
<p>Reversals and recoveries. A bonus paid and then clawed back needs a signed adjustment that triggers a tax recompute. A blind negative earning with no recompute leaves Form 138 and the employee's Form 130 out of step.</p>
<p>Rounding. Decide whether monthly TDS rounds to the nearest rupee and where the leftover paise go (usually March). Write the rule down, or two developers will implement two different ones.</p>
<h3>A validation checklist for engineers</h3>
<ol>
<li>Store the annual tax estimate and the residual after every run, not only the TDS figure on the payslip.</li>
<li>Log a hash of the inputs: regime, annual taxable income, TDS to date, months remaining, bonus to date.</li>
<li>When bonus-to-date changes, force a full recompute before the payslip is locked.</li>
<li>Check that March TDS plus TDS to date matches the final projected tax within your rounding tolerance.</li>
<li>Reconcile the quarterly Form 138 export against the taxable income and TDS totals your engine used each month.</li>
</ol>
<h3>What payroll should tell the employee</h3>
<p>A short, plain reply works better than a tax lecture:</p>
<p>"Your bonus raised your estimated tax for the full year. Your income for the year is now above the level where the rebate applies, so tax that was zero before is now due. We spread the balance over the months left in the tax year, which is why TDS stays higher even in months with no bonus. In June 2027 you will receive Form 130 (it replaced Form 16 from tax year 2026-27). Any remaining difference is settled when you file your income-tax return."</p>
<p>If your process allows it, attach the annual projection sheet. That one PDF closes most of these tickets.</p>
<h3>What to store in the engine</h3>
<p>When TDS jumps after a bonus, the annual projection is usually working as designed. Payroll engines that store the projection, the residual and the remaining month count make support and compliance easier, because every number on the payslip can be traced back. We build this into Engage HRMS (<a href="https://hrengage.ai">https://hrengage.ai</a>): the projection ledger is a stored record that survives the payroll run, not a temporary variable inside the payslip job.</p>
<h3>Disclaimer</h3>
<p>The example uses rounded figures to show the method. Real TDS depends on the employee's regime, declared deductions, and the rates in force for the tax year. Check current Income Tax Department guidance before coding production rates.</p>
<h3>Sources</h3>
<ul>
<li>Income-tax Act, 2025, section 202 (new-regime rates for individuals and others for the tax year): nil up to Rs 4,00,000; 5% on Rs 4,00,001 to Rs 8,00,000; 10% on Rs 8,00,001 to Rs 12,00,000; 15% on Rs 12,00,001 to Rs 16,00,000 (and higher slabs above). Department section PDF mirror: <a href="https://crosscheckai.in/pdf/ita-2025/s202.pdf">https://crosscheckai.in/pdf/ita-2025/s202.pdf</a></li>
<li>Income-tax Act, 2025, section 19 (standard deduction from salaries): Rs 75,000 or the salary, whichever is less, where tax is computed under section 202(1). Section text: <a href="https://incometax.futani.in/act/section.php?section=19">https://incometax.futani.in/act/section.php?section=19</a></li>
<li>Income-tax Act, 2025, section 156 (rebate for certain individuals under the new regime): where income does not exceed twelve lakh rupees, 100% of income-tax payable or Rs 60,000, whichever is less. Section text: <a href="https://incometax.futani.in/act/section.php?section=156">https://incometax.futani.in/act/section.php?section=156</a></li>
<li>Union Budget Memorandum / Finance Act, 2026 materials for tax year 2026-27: Health and Education Cess on income-tax at 4% on income-tax inclusive of surcharge (indiabudget.gov.in/doc/memo.pdf)</li>
<li>Income Tax Department, Form 130 and Form 138 FAQs: <a href="https://www.incometaxindia.gov.in/documents/d/guest/form-130-faqs">https://www.incometaxindia.gov.in/documents/d/guest/form-130-faqs</a> and <a href="https://www.incometaxindia.gov.in/documents/d/guest/form-138-faqs">https://www.incometaxindia.gov.in/documents/d/guest/form-138-faqs</a></li>
</ul>
]]></content:encoded></item></channel></rss>