PHPackages                             crmleaf/payroll-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. crmleaf/payroll-core

ActiveLibrary

crmleaf/payroll-core
====================

Framework-agnostic Indian payroll calculation engine: EPF, ESI, TDS, gratuity, bonus, professional tax and settlement maths, with versioned statutory rate tables.

v1.0.0(2d ago)0105↑2642.9%16MITPHPPHP ^8.2CI passing

Since Aug 13Pushed 2d agoCompare

[ Source](https://github.com/CrmLeaf/payroll-core)[ Packagist](https://packagist.org/packages/crmleaf/payroll-core)[ Docs](https://github.com/crmleaf/payroll-core)[ RSS](/packages/crmleaf-payroll-core/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (3)Versions (2)Used By (16)

crmleaf/payroll-core
====================

[](#crmleafpayroll-core)

[![CI](https://github.com/crmleaf/payroll-core/actions/workflows/ci.yml/badge.svg)](https://github.com/crmleaf/payroll-core/actions/workflows/ci.yml)[![Packagist](https://camo.githubusercontent.com/356c23cd28a149ceb3342da44e10a51cf63667bb1b30ce3604fdf8862a8c1536/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f63726d6c6561662f706179726f6c6c2d636f7265)](https://packagist.org/packages/crmleaf/payroll-core)[![PHP](https://camo.githubusercontent.com/547ebf046079fd65e01ad00ec395110b6f9fbb5cd5d01b5f358e828ce7e02569/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f63726d6c6561662f706179726f6c6c2d636f72652f706870)](https://www.php.net/supported-versions)[![Licence](https://camo.githubusercontent.com/197e81d6854fe99eed0a41f88970bc8c0936e0e11af410f5934a7e8db807e2e5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f63726d6c6561662f706179726f6c6c2d636f7265)](LICENSE)

Indian payroll and statutory calculations in plain PHP. No framework, no dependencies beyond `ext-json`, and every figure comes back with the working and the statute behind it.

```
composer require crmleaf/payroll-core
```

This is the engine the [CRMLeaf payroll tools](https://github.com/crmleaf) are built on. Each tool is a thin package over one calculator here; this is where the arithmetic and the rate data live.

Why it exists
-------------

[](#why-it-exists)

Indian payroll arithmetic is not difficult, but it is fiddly, and it is wrong in a lot of software:

- The pension share is capped at the wage ceiling even when provident fund is not, so 8.33% of a ₹30,000 basic is ₹1,250, not ₹2,499.
- State insurance contributions continue to the end of a contribution period after wages cross the limit, so applicability cannot be read off the wage.
- Gratuity rounds a part year up only past six months. Seven years and six months is seven; seven and seven is eight.
- The section 87A rebate has a marginal relief band. At ₹12,10,000 of taxable income the slabs give ₹61,500 and the payable figure is ₹10,000.

Each of those is a solved problem that gets re-solved badly in private. This solves them once, in the open, with the citation next to the number.

Important

This is a calculation library, not tax advice. It implements our reading of the applicable statutes and is provided without warranty. Verify against your own compliance obligations before relying on it for a filing.

Use it
------

[](#use-it)

```
use Crmleaf\Payroll\Calculators\GratuityCalculator;
use Crmleaf\Payroll\Money;

$result = (new GratuityCalculator())->calculate(
    lastDrawnSalary: Money::fromRupees(45_000),   // basic + DA
    yearsOfService: 7,
    monthsOfService: 8,                           // over six months, so eight years
);

$result->gratuity->format();      // '₹2,07,692.31'
$result->completedYears;          // 8
$result->explain();               // '(15 × 45,000.00 × 8) ÷ 26 = ₹2,07,692.31'
$result->taxExempt->toRupees();   // 207692.31
```

Every calculator is a `final class`, constructible with `new` and no arguments, with a single `calculate()` taking named parameters.

### Every result shows its working

[](#every-result-shows-its-working)

```
foreach ($result->steps() as $step) {
    echo $step->label, ': ', $step->formula, ' → ', $step->amount?->format(), "\n";
    echo '  ', $step->citation, "\n";
}
```

A payroll figure has to be defensible. An employee asking why ₹1,250 went to the pension fund rather than ₹2,499 is entitled to the reasoning, and so is an auditor, so the reasoning is part of the return value rather than something you reconstruct afterwards.

`toArray()` gives the same thing as snake\_case scalars, which is what the JSON API and the Blade components consume.

What is in it
-------------

[](#what-is-in-it)

Provident fundEmployee, employer, pension and insurance shares with the wage ceilingState insuranceBoth shares, the wage limit, and the contribution-period ruleIncome taxBoth regimes, slabs, 87A with marginal relief, surcharge, cessTax deducted at sourceMonthly instalments, mid-year joiners, regime comparisonProfessional taxEvery state that levies it, at its own frequencyGratuityCovered and uncovered establishments, six-month rounding, exemptionBonusEligibility limit and calculation ceiling, kept distinctLeave encashmentThe section 10(10AA) least-of-four testCost to companyA full structure, with the components reconciling to the totalFull and final settlementDues, gratuity, leave, notice recovery, deductionsProvident fund penaltiesSection 7Q interest and 14B damages, on both basesCompliance calendarStatutory due dates, as an iCalendar feedSalary templatesStructure, register, slip and master, as column schemasTwo more answer a commercial question rather than a statutory one, so no rate table governs them and neither takes a date:

Return on investmentA payroll cycle run by hand against the same cycle automatedSavingsTwo per-employee costs compared across the length of a contractMoney is an integer of paise
----------------------------

[](#money-is-an-integer-of-paise)

```
Money::fromRupees(15_000)->percentage(8.33)->paise;   // 124950, exactly
Money::fromRupees(1_234_567.89)->format();            // '₹12,34,567.89'
```

Payroll divides by 26 for gratuity, takes 8.33% for the pension share, and spreads an annual tax figure over twelve months. In floats that accumulates error which eventually surfaces as a one-rupee mismatch on a challan, so every amount is an integer number of paise and every division rounds explicitly.

The statutes disagree about rounding, so there are three named methods rather than one: `roundUpToRupee()` for state insurance, `roundToRupee()` for tax deducted at source, and `roundToTenRupees()` for section 288B. Each is applied where the law says so.

Rates are dated data, not code
------------------------------

[](#rates-are-dated-data-not-code)

Nothing here is "the current rate". Everything is "the rate on this date", because payroll routinely recomputes the past: a revised settlement six months after separation, an arrear paid in a later year, an audit of last year's challans.

```
(new PfCalculator())->calculate(
    basicSalary: Money::fromRupees(30_000),
    asOf: '2013-06-01',
)->wageCeiling->toRupees();     // 6500 - the ceiling before it was raised in 2014
```

Ten tables live in `resources/rates/`, each version carrying its own `effective_from`, `effective_to` and a cited `source`. Changing a rate means adding a dated version, never editing a past one.

`asOf` accepts a `DateTimeImmutable`, a `YYYY-MM-DD` string, or a financial-year label such as `'2025-26'`.

### When a year's rates have not been published yet

[](#when-a-years-rates-have-not-been-published-yet)

A Finance Act arrives after the financial year it governs has begun, so for part of every year the newest income tax table names the *previous* year. Payroll cannot stop and wait for a gazette, so the table is carried forward - and the result says so rather than letting you assume otherwise:

```
$result = (new IncomeTaxCalculator())->calculate(
    grossSalary: Money::fromRupees(12_00_000),
);

$result->notes;
// ['Rates for FY 2025-26 are being applied to a date in FY 2026-27, because no
//   version has been published for FY 2026-27 yet. Verify against the current
//   Finance Act before relying on this figure for filing.']
```

The same warning is reachable directly with `RateTable::isProvisionalFor()` and `provisionalWarning()`. Only the income tax table is dated by financial year; the others change by notification and carry no such assumption.

Ineligibility is a result, not an error
---------------------------------------

[](#ineligibility-is-a-result-not-an-error)

Three years of service is a valid answer of nil gratuity, with a reason. A negative salary is a bug in the caller and throws.

```
$result = (new GratuityCalculator())->calculate(
    lastDrawnSalary: Money::fromRupees(45_000),
    yearsOfService: 3,
);

$result->eligible;              // false
$result->gratuity->isZero();    // true
$result->ineligibilityReason;   // 'Gratuity needs 5 years of continuous service. …'
```

Also available
--------------

[](#also-available)

- **[`crmleaf/laravel-payroll`](https://github.com/crmleaf/laravel-payroll)** - service provider, publishable config, an opt-in JSON API, Blade components and PDF documents.
- **[`@crmleaf/payroll-js`](https://github.com/crmleaf/payroll-js)** - the same calculations in TypeScript, reading the same rate tables, agreeing to the paisa. A CI job fails the build if the two ever drift.
- **One package per tool** - `crmleaf/gratuity-calculator` and the rest, if you want a single calculator with its own routes and views.

Contributing
------------

[](#contributing)

Issues and pull requests both belong here. See [CONTRIBUTING.md](CONTRIBUTING.md) for how a rate change is dated and cited.

A wrong figure is the most valuable kind of report: give the inputs, the figure you got, the figure you expected, and the rule that says so. The first three make it reproducible; the fourth makes it a test case.

Licence
-------

[](#licence)

[MIT](LICENSE) © CRMLeaf.

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance100

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community16

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

2d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/315788704?v=4)[crmleaf](/maintainers/crmleaf)[@CrmLeaf](https://github.com/CrmLeaf)

---

Top Contributors

[![andolasoftuser196](https://avatars.githubusercontent.com/u/145565449?v=4)](https://github.com/andolasoftuser196 "andolasoftuser196 (23 commits)")

---

Tags

epfesigratuityincome-taxindiapayrollphpstatutory-complianceesiindiatdspayrollsalaryEPFBonusgratuityprofessional-tax

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/crmleaf-payroll-core/health.svg)

```
[![Health](https://phpackages.com/badges/crmleaf-payroll-core/health.svg)](https://phpackages.com/packages/crmleaf-payroll-core)
```

###  Alternatives

[friendsofsymfony/http-cache-bundle

Set path based HTTP cache headers and send invalidation requests to your HTTP cache

43714.1M78](/packages/friendsofsymfony-http-cache-bundle)[barryvdh/laravel-httpcache

HttpCache for Laravel

502407.5k12](/packages/barryvdh-laravel-httpcache)[razorpay/magento

Razorpay Magento 2.0 plugin for accepting payments.

3080.1k1](/packages/razorpay-magento)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
