@ph-dev-utils/payroll 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @ph-dev-utils/payroll
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@ph-dev-utils/payroll?label=npm&color=cb3837&logo=npm)](https://www.npmjs.com/package/@ph-dev-utils/payroll)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kon2raya24/ph-payroll/blob/main/LICENSE)
5
+ [![Made in PH](https://img.shields.io/badge/made%20in-πŸ‡΅πŸ‡­%20Philippines-0038A8)](https://github.com/kon2raya24)
6
+
7
+ Filipino payroll calculators for JavaScript / TypeScript β€” SSS, PhilHealth, Pag-IBIG monthly contributions, 13th-month pay, and pre-tax net take-home. Tables versioned by effective date.
8
+
9
+ ## ⚠️ READ FIRST
10
+
11
+ This package handles **real money math**. Wrong outputs cause underpayment / overpayment in actual payroll runs. Verify outputs against official agency calculators before production use. Pin your dependency version β€” tables change every few years. **BIR withholding tax is NOT included in v0.1** (deferred to v0.2 to avoid shipping a naΓ―ve WT calculator).
12
+
13
+ Full disclaimer: [project README](https://github.com/kon2raya24/ph-payroll#-read-first--accuracy-disclaimer).
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @ph-dev-utils/payroll
19
+ ```
20
+
21
+ Requires Node 20+.
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import { netTakeHome } from '@ph-dev-utils/payroll';
27
+
28
+ netTakeHome(30000);
29
+ // {
30
+ // gross: 30000,
31
+ // sss: { msc: 30000, employeeShare: 1500, employerShare: 3030, ... },
32
+ // philHealth: { total: 1500, employee: 750, employer: 750 },
33
+ // pagIbig: { mfs: 10000, employee: 200, employer: 200, total: 400 },
34
+ // totalDeductions: 2450,
35
+ // net: 27550
36
+ // }
37
+ ```
38
+
39
+ ## API Reference
40
+
41
+ ### `sssContribution(monthlyCompensation, opts?)`
42
+
43
+ Source: **SSS Circular 2024-006** (effective 2025-01-01, applies to 2025–2026).
44
+
45
+ ```ts
46
+ function sssContribution(
47
+ monthlyCompensation: number,
48
+ opts?: { year?: number }
49
+ ): SSSContribution
50
+
51
+ interface SSSContribution {
52
+ msc: number; // Monthly Salary Credit (β‚±500 increments, clamped 5,000–35,000)
53
+ regular: { employee: number; employer: number; total: number }; // MSC capped at 20,000
54
+ mpf: { employee: number; employer: number; total: number }; // MySSS Pension Booster β€” portion above 20,000
55
+ ec: number; // Employees' Compensation (employer-paid)
56
+ employeeShare: number; // regular.employee + mpf.employee
57
+ employerShare: number; // regular.employer + mpf.employer + ec
58
+ total: number;
59
+ }
60
+ ```
61
+
62
+ **Math:** Total rate 15% = 10% employer + 5% employee. EC is β‚±10 if MSC ≀ 14,500, β‚±30 if MSC β‰₯ 15,000.
63
+
64
+ ```ts
65
+ sssContribution(20000);
66
+ // { msc: 20000, regular: { employee: 1000, employer: 2000, total: 3000 },
67
+ // mpf: { employee: 0, employer: 0, total: 0 }, ec: 30,
68
+ // employeeShare: 1000, employerShare: 2030, total: 3030 }
69
+
70
+ sssContribution(30000);
71
+ // MPF kicks in: regular at MSC=20k, MPF at MSC=10k
72
+ // { msc: 30000, regular: { employee: 1000, ... }, mpf: { employee: 500, employer: 1000, total: 1500 },
73
+ // ec: 30, employeeShare: 1500, employerShare: 3030, total: 4530 }
74
+
75
+ sssContribution(50000).msc; // 35000 (capped at MSC max)
76
+ sssContribution(3000).msc; // 5000 (floored at MSC min)
77
+ ```
78
+
79
+ ---
80
+
81
+ ### `philHealthContribution(monthlySalary, opts?)`
82
+
83
+ Source: **RA 11223 (UHC Law) final 5% rate** (applies 2024+; no further increases scheduled).
84
+
85
+ ```ts
86
+ function philHealthContribution(
87
+ monthlySalary: number,
88
+ opts?: { year?: number }
89
+ ): { total: number; employee: number; employer: number }
90
+ ```
91
+
92
+ **Math:** 5% of monthly salary, split equally (2.5% each). Floor β‚±500 total (salary ≀ β‚±10,000). Cap β‚±5,000 total (salary β‰₯ β‚±100,000). Each share is rounded to the centavo; the remitted total is the sum of remitted shares (real-payroll convention).
93
+
94
+ ```ts
95
+ philHealthContribution(25000); // { total: 1250, employee: 625, employer: 625 }
96
+ philHealthContribution(5000); // floor: { total: 500, employee: 250, employer: 250 }
97
+ philHealthContribution(150000); // ceiling: { total: 5000, employee: 2500, employer: 2500 }
98
+ ```
99
+
100
+ ---
101
+
102
+ ### `pagIbigContribution(monthlySalary, opts?)`
103
+
104
+ Source: **HDMF Circular 460** (effective 2024-02-01). MFS ceiling raised from β‚±5,000 to β‚±10,000 in Feb 2024.
105
+
106
+ ```ts
107
+ function pagIbigContribution(
108
+ monthlySalary: number,
109
+ opts?: { year?: number }
110
+ ): { mfs: number; employee: number; employer: number; total: number }
111
+ ```
112
+
113
+ **Math:**
114
+ - MFS = min(monthlySalary, 10,000)
115
+ - If MFS ≀ β‚±1,500: employee 1%, employer 2%
116
+ - If MFS > β‚±1,500: employee 2%, employer 2%
117
+ - Max β‚±200 each side (at MFS = β‚±10,000)
118
+
119
+ Mandatory contribution only. Voluntary higher contributions are **not** computed here.
120
+
121
+ ```ts
122
+ pagIbigContribution(1000); // low bracket: { mfs: 1000, employee: 10, employer: 20, total: 30 }
123
+ pagIbigContribution(5000); // high bracket: { mfs: 5000, employee: 100, employer: 100, total: 200 }
124
+ pagIbigContribution(50000); // MFS capped: { mfs: 10000, employee: 200, employer: 200, total: 400 }
125
+ ```
126
+
127
+ ---
128
+
129
+ ### `thirteenthMonthPay(totalBasicEarnings)` / `thirteenthMonthFromMonthly(monthlyBasicSalary, monthsWorked?)`
130
+
131
+ Source: **PD 851** (Presidential Decree on 13th-month pay).
132
+
133
+ ```ts
134
+ function thirteenthMonthPay(totalBasicEarnings: number): number
135
+ function thirteenthMonthFromMonthly(monthlyBasicSalary: number, monthsWorked?: number): number
136
+ ```
137
+
138
+ **Math:** DOLE canonical formula is `totalBasicEarnings / 12`. The "proration" for partial-year employees is captured by passing actual earnings β€” not by post-multiplying.
139
+
140
+ `thirteenthMonthFromMonthly` is a convenience for fixed-salary employees: equivalent to `thirteenthMonthPay(monthlyBasicSalary Γ— monthsWorked)`.
141
+
142
+ **"Basic earnings" excludes** allowances, overtime, holiday pay, night-shift differential, and other non-base monetary benefits. Caller is responsible for passing the right figure.
143
+
144
+ ```ts
145
+ thirteenthMonthPay(360000); // 30000 (full year @ 30k/month)
146
+ thirteenthMonthPay(180000); // 15000 (6 months @ 30k/month)
147
+ thirteenthMonthFromMonthly(30000); // 30000 (defaults to 12 months)
148
+ thirteenthMonthFromMonthly(30000, 6); // 15000
149
+ thirteenthMonthFromMonthly(30000, 1); // 2500
150
+ ```
151
+
152
+ ---
153
+
154
+ ### `netTakeHome(monthlySalary, opts?)`
155
+
156
+ Pre-tax net take-home: gross salary minus mandatory SSS / PhilHealth / Pag-IBIG employee contributions.
157
+
158
+ ```ts
159
+ function netTakeHome(
160
+ monthlySalary: number,
161
+ opts?: { year?: number }
162
+ ): NetTakeHome
163
+
164
+ interface NetTakeHome {
165
+ gross: number;
166
+ sss: SSSContribution;
167
+ philHealth: PhilHealthContribution;
168
+ pagIbig: PagIbigContribution;
169
+ totalDeductions: number; // sum of employee shares
170
+ net: number; // gross - totalDeductions
171
+ }
172
+ ```
173
+
174
+ **Important:** This is **pre-tax** net. It does NOT subtract BIR withholding tax. To get true take-home, you'd subtract WT yourself (using a tax engine that handles taxable-income calculation, per-period brackets, de minimis benefits, etc.).
175
+
176
+ ```ts
177
+ netTakeHome(30000).net; // 27550
178
+ netTakeHome(8000).net; // 7190 (low-earner with floor PhilHealth + EC)
179
+ netTakeHome(150000).net; // 145550 (capped contributions)
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Versioning & rate changes
185
+
186
+ Tables change. The package follows this contract:
187
+
188
+ - Within a major+minor version (e.g. `0.1.x`), tables won't change.
189
+ - A new table ships in a new minor version (`0.2.0`).
190
+ - Old tables stay accessible via the `{ year }` option until a major version cleanup.
191
+
192
+ **Pin your dependency** to a specific minor (`~0.1.0` or `0.1.x`) to avoid silent table changes during your payroll cycle.
193
+
194
+ When a circular drops, file an issue with the source link β€” patches ship quickly.
195
+
196
+ ## License
197
+
198
+ MIT
package/data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # ph-payroll data
2
+
3
+ This directory holds the contribution-rate / bracket tables that drive the payroll calculators. Each file is **versioned by effective date** and includes its source citation.
4
+
5
+ ## Files
6
+
7
+ | File | Authority | Effective from | Applies to |
8
+ |---|---|---|---|
9
+ | `sss-table-2025.json` | SSS Circular 2024-006 (RA 11199) | 2025-01-01 | 2025, 2026 |
10
+ | `philhealth-rate-2026.json` | UHC Law final adjustment (RA 11223) | 2024-01-01 | 2024, 2025, 2026 |
11
+ | `pagibig-rate-2024.json` | HDMF Circular 460 (RA 9679) | 2024-02-01 | 2024, 2025, 2026 |
12
+
13
+ ## Why this layout
14
+
15
+ Rates **change**. The SSS rate moved 13% β†’ 14% β†’ 15% between 2023 and 2025. PhilHealth moved 4% β†’ 4.5% β†’ 5% between 2023 and 2025. Pag-IBIG raised the MFS cap from β‚±5,000 to β‚±10,000 in February 2024.
16
+
17
+ This package keeps each version of each table as its own JSON file with explicit `effective_from` / `effective_until` metadata. Calculator functions accept an optional `{ year }` parameter β€” pass it and the right table loads. Default is the most recent table.
18
+
19
+ When a new circular issues, the workflow is:
20
+ 1. Add a new file `sss-table-YYYY.json` (or equivalent).
21
+ 2. Set the previous table's `effective_until` to the day before the new one.
22
+ 3. Update the README table above.
23
+ 4. Bump the package version.
24
+
25
+ The old file stays in the repo β€” users who haven't upgraded keep using their pinned version with the older table.
26
+
27
+ ## Accuracy caveat
28
+
29
+ The official SSS contribution table is published as **images** on sss.gov.ph, not as a structured document. The bracket boundaries in this package are reproduced algorithmically (β‚±500 MSC increments, rounded to nearest from the compensation figure). If you spot a discrepancy against an official SSS-issued payslip, please file an issue with the example.
30
+
31
+ PhilHealth and Pag-IBIG rates are simple percentage formulas with floor / ceiling β€” no bracket table.
32
+
33
+ ## If a rate changed and we missed it
34
+
35
+ Open an issue with:
36
+ 1. Link to the official circular (SSS / PhilHealth / Pag-IBIG / BIR)
37
+ 2. The effective date
38
+ 3. The specific bracket or rate that changed
39
+
40
+ We'd rather know fast and ship a patch than have wrong numbers in production payrolls.
@@ -0,0 +1,38 @@
1
+ {
2
+ "_meta": {
3
+ "effective_from": "2024-02-01",
4
+ "effective_until": null,
5
+ "applies_to_years": [2024, 2025, 2026],
6
+ "source": "Pag-IBIG HDMF Circular 460 (and subsequent)",
7
+ "source_url": "https://www.pagibigfund.gov.ph/",
8
+ "authority": "Republic Act No. 9679 (HDMF Law of 2009), as amended",
9
+ "verified_on": "2026-05-19",
10
+ "notes": [
11
+ "Effective February 2024: maximum Monthly Fund Salary (MFS) raised from β‚±5,000 to β‚±10,000.",
12
+ "Employee/employer rates depend on MFS bracket.",
13
+ "Mandatory contribution is capped; members may opt to contribute more voluntarily (out of scope for this package)."
14
+ ]
15
+ },
16
+
17
+ "msc": {
18
+ "_alias": "MFS β€” Monthly Fund Salary",
19
+ "cap": 10000,
20
+ "_comment": "MFS = min(monthlyCompensation, cap)."
21
+ },
22
+
23
+ "brackets": [
24
+ {
25
+ "mfs_upper_inclusive": 1500,
26
+ "employee_percent": 0.01,
27
+ "employer_percent": 0.02
28
+ },
29
+ {
30
+ "mfs_upper_inclusive": null,
31
+ "_upper_comment": "null = no upper, but MFS is capped at msc.cap (β‚±10,000)",
32
+ "employee_percent": 0.02,
33
+ "employer_percent": 0.02
34
+ }
35
+ ],
36
+
37
+ "_max_contribution_each_comment": "At MFS = β‚±10,000, employee = 10,000 Γ— 2% = β‚±200; employer = 10,000 Γ— 2% = β‚±200."
38
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "_meta": {
3
+ "effective_from": "2024-01-01",
4
+ "effective_until": null,
5
+ "applies_to_years": [2024, 2025, 2026],
6
+ "rate_is_final": true,
7
+ "_rate_is_final_comment": "5% is the final scheduled adjustment under RA 11223 (Universal Health Care Act). No further increases planned after 2026.",
8
+ "source": "PhilHealth Circular / PIA announcement",
9
+ "source_url": "https://pia.gov.ph/news/philhealth-sets-5-premium-contribution-rate-for-2026/",
10
+ "authority": "Republic Act No. 11223 (Universal Health Care Act)",
11
+ "verified_on": "2026-05-19"
12
+ },
13
+
14
+ "rate": {
15
+ "total_percent": 0.05,
16
+ "employer_percent": 0.025,
17
+ "employee_percent": 0.025
18
+ },
19
+
20
+ "floor": {
21
+ "salary_threshold_inclusive": 10000,
22
+ "monthly_premium": 500,
23
+ "_comment": "Members with monthly basic salary ≀ β‚±10,000 pay a fixed β‚±500 total (β‚±250 each side)."
24
+ },
25
+
26
+ "ceiling": {
27
+ "salary_threshold_inclusive": 100000,
28
+ "monthly_premium": 5000,
29
+ "_comment": "Members with monthly basic salary β‰₯ β‚±100,000 pay capped β‚±5,000 total (β‚±2,500 each side)."
30
+ }
31
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "_meta": {
3
+ "effective_from": "2025-01-01",
4
+ "effective_until": null,
5
+ "applies_to_years": [2025, 2026],
6
+ "source": "SSS Circular 2024-006 (Employer / Employee)",
7
+ "source_url": "https://www.sss.gov.ph/sss-contribution-table/",
8
+ "authority": "Republic Act No. 11199 (Social Security Act of 2018)",
9
+ "verified_on": "2026-05-19",
10
+ "notes": [
11
+ "Official SSS bracket table is published as image only on sss.gov.ph; algorithmic MSC rounding (β‚±500 increments, rounded to nearest) reproduces the bracketing.",
12
+ "Total contribution rate: 15% of MSC. Employer share: 10%. Employee share: 5%.",
13
+ "EC (Employees' Compensation) is employer-paid only.",
14
+ "Contributions on MSC portion above β‚±20,000 are allocated to MySSS Pension Booster (mandatory provident fund, MPF) β€” separate sub-account but still part of payroll deduction."
15
+ ]
16
+ },
17
+
18
+ "rate": {
19
+ "total_percent": 0.15,
20
+ "employer_percent": 0.10,
21
+ "employee_percent": 0.05
22
+ },
23
+
24
+ "msc": {
25
+ "min": 5000,
26
+ "max": 35000,
27
+ "increment": 500,
28
+ "regular_ss_cap": 20000,
29
+ "_comment": "MSC = round(monthlyCompensation / 500) * 500, clamped to [min, max]. Portion of MSC above regular_ss_cap goes to MPF."
30
+ },
31
+
32
+ "ec": {
33
+ "_comment": "Employees' Compensation β€” employer-paid only.",
34
+ "low_amount": 10,
35
+ "high_amount": 30,
36
+ "low_msc_threshold_inclusive": 14500,
37
+ "_threshold_comment": "EC = low_amount if MSC ≀ 14,500; EC = high_amount if MSC β‰₯ 15,000."
38
+ }
39
+ }
@@ -0,0 +1,9 @@
1
+ export { sssContribution } from './sss.js';
2
+ export type { SSSContribution, SSSOptions } from './sss.js';
3
+ export { philHealthContribution } from './philhealth.js';
4
+ export type { PhilHealthContribution, PhilHealthOptions } from './philhealth.js';
5
+ export { pagIbigContribution } from './pagibig.js';
6
+ export type { PagIbigContribution, PagIbigOptions } from './pagibig.js';
7
+ export { thirteenthMonthPay, thirteenthMonthFromMonthly } from './thirteenth-month.js';
8
+ export { netTakeHome } from './take-home.js';
9
+ export type { NetTakeHome, NetTakeHomeOptions } from './take-home.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { sssContribution } from './sss.js';
2
+ export { philHealthContribution } from './philhealth.js';
3
+ export { pagIbigContribution } from './pagibig.js';
4
+ export { thirteenthMonthPay, thirteenthMonthFromMonthly } from './thirteenth-month.js';
5
+ export { netTakeHome } from './take-home.js';
@@ -0,0 +1,30 @@
1
+ export interface PagIbigContribution {
2
+ /** Monthly Fund Salary (capped at the table's MFS cap). */
3
+ mfs: number;
4
+ /** Employee share. */
5
+ employee: number;
6
+ /** Employer share. */
7
+ employer: number;
8
+ /** Total monthly contribution. */
9
+ total: number;
10
+ }
11
+ export interface PagIbigOptions {
12
+ /** Year β€” must be covered by the loaded table. Default: most recent. */
13
+ year?: number;
14
+ }
15
+ /**
16
+ * Compute Pag-IBIG (HDMF) monthly contribution.
17
+ * Source: HDMF Circular 460 and subsequent (effective 2024-02-01).
18
+ * Voluntary higher contributions are NOT calculated here β€” this is mandatory only.
19
+ *
20
+ * @param monthlySalary Gross monthly compensation in pesos.
21
+ * @param opts.year Year of the desired table (default: most recent supported).
22
+ *
23
+ * @example
24
+ * pagIbigContribution(15000);
25
+ * // MFS capped at 10,000 β†’ { mfs: 10000, employee: 200, employer: 200, total: 400 }
26
+ *
27
+ * pagIbigContribution(1000);
28
+ * // Low bracket (1% emp / 2% er): { mfs: 1000, employee: 10, employer: 20, total: 30 }
29
+ */
30
+ export declare function pagIbigContribution(monthlySalary: number, opts?: PagIbigOptions): PagIbigContribution;
@@ -0,0 +1,44 @@
1
+ import table from '../data/pagibig-rate-2024.json' with { type: 'json' };
2
+ function round2(n) {
3
+ return Math.round(n * 100) / 100;
4
+ }
5
+ /**
6
+ * Compute Pag-IBIG (HDMF) monthly contribution.
7
+ * Source: HDMF Circular 460 and subsequent (effective 2024-02-01).
8
+ * Voluntary higher contributions are NOT calculated here β€” this is mandatory only.
9
+ *
10
+ * @param monthlySalary Gross monthly compensation in pesos.
11
+ * @param opts.year Year of the desired table (default: most recent supported).
12
+ *
13
+ * @example
14
+ * pagIbigContribution(15000);
15
+ * // MFS capped at 10,000 β†’ { mfs: 10000, employee: 200, employer: 200, total: 400 }
16
+ *
17
+ * pagIbigContribution(1000);
18
+ * // Low bracket (1% emp / 2% er): { mfs: 1000, employee: 10, employer: 20, total: 30 }
19
+ */
20
+ export function pagIbigContribution(monthlySalary, opts = {}) {
21
+ if (!Number.isFinite(monthlySalary) || monthlySalary < 0) {
22
+ throw new TypeError('pagIbigContribution: monthlySalary must be a non-negative number');
23
+ }
24
+ if (opts.year !== undefined && !table._meta.applies_to_years.includes(opts.year)) {
25
+ throw new RangeError(`pagIbigContribution: no Pag-IBIG table available for year ${opts.year}. Available: ${table._meta.applies_to_years.join(', ')}.`);
26
+ }
27
+ const mfs = Math.min(monthlySalary, table.msc.cap);
28
+ // Find applicable bracket. Brackets are ordered low β†’ high; first whose upper bound contains MFS wins.
29
+ let bracket = table.brackets[table.brackets.length - 1];
30
+ for (const b of table.brackets) {
31
+ if (b.mfs_upper_inclusive !== null && mfs <= b.mfs_upper_inclusive) {
32
+ bracket = b;
33
+ break;
34
+ }
35
+ }
36
+ const employee = round2(mfs * bracket.employee_percent);
37
+ const employer = round2(mfs * bracket.employer_percent);
38
+ return {
39
+ mfs,
40
+ employee,
41
+ employer,
42
+ total: round2(employee + employer),
43
+ };
44
+ }
@@ -0,0 +1,28 @@
1
+ export interface PhilHealthContribution {
2
+ /** Total monthly premium (employee + employer). */
3
+ total: number;
4
+ /** Employee share. */
5
+ employee: number;
6
+ /** Employer share. */
7
+ employer: number;
8
+ }
9
+ export interface PhilHealthOptions {
10
+ /** Year β€” must be covered by the loaded table. Default: most recent. */
11
+ year?: number;
12
+ }
13
+ /**
14
+ * Compute PhilHealth monthly premium contribution.
15
+ * Source: PhilHealth Circular per UHC Law (RA 11223). 5% is the final scheduled
16
+ * adjustment β€” no further increases are scheduled after 2026.
17
+ *
18
+ * @param monthlySalary Gross monthly basic salary in pesos.
19
+ * @param opts.year Year of the desired table (default: most recent supported).
20
+ *
21
+ * @example
22
+ * philHealthContribution(25000);
23
+ * // { total: 1250, employee: 625, employer: 625 }
24
+ *
25
+ * philHealthContribution(5000); // floor: { total: 500, employee: 250, employer: 250 }
26
+ * philHealthContribution(150000); // ceiling: { total: 5000, employee: 2500, employer: 2500 }
27
+ */
28
+ export declare function philHealthContribution(monthlySalary: number, opts?: PhilHealthOptions): PhilHealthContribution;
@@ -0,0 +1,44 @@
1
+ import table from '../data/philhealth-rate-2026.json' with { type: 'json' };
2
+ function round2(n) {
3
+ return Math.round(n * 100) / 100;
4
+ }
5
+ /**
6
+ * Compute PhilHealth monthly premium contribution.
7
+ * Source: PhilHealth Circular per UHC Law (RA 11223). 5% is the final scheduled
8
+ * adjustment β€” no further increases are scheduled after 2026.
9
+ *
10
+ * @param monthlySalary Gross monthly basic salary in pesos.
11
+ * @param opts.year Year of the desired table (default: most recent supported).
12
+ *
13
+ * @example
14
+ * philHealthContribution(25000);
15
+ * // { total: 1250, employee: 625, employer: 625 }
16
+ *
17
+ * philHealthContribution(5000); // floor: { total: 500, employee: 250, employer: 250 }
18
+ * philHealthContribution(150000); // ceiling: { total: 5000, employee: 2500, employer: 2500 }
19
+ */
20
+ export function philHealthContribution(monthlySalary, opts = {}) {
21
+ if (!Number.isFinite(monthlySalary) || monthlySalary < 0) {
22
+ throw new TypeError('philHealthContribution: monthlySalary must be a non-negative number');
23
+ }
24
+ if (opts.year !== undefined && !table._meta.applies_to_years.includes(opts.year)) {
25
+ throw new RangeError(`philHealthContribution: no PhilHealth table available for year ${opts.year}. Available: ${table._meta.applies_to_years.join(', ')}.`);
26
+ }
27
+ let total;
28
+ if (monthlySalary <= table.floor.salary_threshold_inclusive) {
29
+ total = table.floor.monthly_premium;
30
+ }
31
+ else if (monthlySalary >= table.ceiling.salary_threshold_inclusive) {
32
+ total = table.ceiling.monthly_premium;
33
+ }
34
+ else {
35
+ total = round2(monthlySalary * table.rate.total_percent);
36
+ }
37
+ // Split equally between employee and employer.
38
+ const each = round2(total / 2);
39
+ return {
40
+ total: round2(each * 2),
41
+ employee: each,
42
+ employer: each,
43
+ };
44
+ }
package/dist/sss.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ export interface SSSContribution {
2
+ /** Monthly Salary Credit (MSC) β€” the bracketed input to SSS math. */
3
+ msc: number;
4
+ /** Regular SS portion (MSC capped at 20,000). */
5
+ regular: {
6
+ employee: number;
7
+ employer: number;
8
+ total: number;
9
+ };
10
+ /** MySSS Pension Booster / MPF β€” portion of MSC above 20,000. Zero if MSC ≀ 20,000. */
11
+ mpf: {
12
+ employee: number;
13
+ employer: number;
14
+ total: number;
15
+ };
16
+ /** Employees' Compensation β€” flat amount, employer-paid only. */
17
+ ec: number;
18
+ /** Total employee deduction: regular.employee + mpf.employee. */
19
+ employeeShare: number;
20
+ /** Total employer cost: regular.employer + mpf.employer + ec. */
21
+ employerShare: number;
22
+ /** Grand total (employee + employer + EC). */
23
+ total: number;
24
+ }
25
+ export interface SSSOptions {
26
+ /** Year β€” must be covered by the loaded table. Default: most recent. */
27
+ year?: number;
28
+ }
29
+ /**
30
+ * Compute SSS monthly contribution for an employed member.
31
+ * Source: SSS Circular 2024-006 (effective 2025-01-01, applies to 2025 and 2026).
32
+ *
33
+ * @param monthlyCompensation Gross monthly compensation in pesos.
34
+ * @param opts.year Year of the desired table (default: most recent supported).
35
+ *
36
+ * @example
37
+ * sssContribution(20000);
38
+ * // { msc: 20000, regular: { employee: 1000, employer: 2000, total: 3000 },
39
+ * // mpf: { employee: 0, employer: 0, total: 0 }, ec: 30,
40
+ * // employeeShare: 1000, employerShare: 2030, total: 3030 }
41
+ */
42
+ export declare function sssContribution(monthlyCompensation: number, opts?: SSSOptions): SSSContribution;
package/dist/sss.js ADDED
@@ -0,0 +1,54 @@
1
+ import table from '../data/sss-table-2025.json' with { type: 'json' };
2
+ function round2(n) {
3
+ return Math.round(n * 100) / 100;
4
+ }
5
+ /**
6
+ * Compute SSS monthly contribution for an employed member.
7
+ * Source: SSS Circular 2024-006 (effective 2025-01-01, applies to 2025 and 2026).
8
+ *
9
+ * @param monthlyCompensation Gross monthly compensation in pesos.
10
+ * @param opts.year Year of the desired table (default: most recent supported).
11
+ *
12
+ * @example
13
+ * sssContribution(20000);
14
+ * // { msc: 20000, regular: { employee: 1000, employer: 2000, total: 3000 },
15
+ * // mpf: { employee: 0, employer: 0, total: 0 }, ec: 30,
16
+ * // employeeShare: 1000, employerShare: 2030, total: 3030 }
17
+ */
18
+ export function sssContribution(monthlyCompensation, opts = {}) {
19
+ if (!Number.isFinite(monthlyCompensation) || monthlyCompensation < 0) {
20
+ throw new TypeError('sssContribution: monthlyCompensation must be a non-negative number');
21
+ }
22
+ if (opts.year !== undefined && !table._meta.applies_to_years.includes(opts.year)) {
23
+ throw new RangeError(`sssContribution: no SSS table available for year ${opts.year}. Available: ${table._meta.applies_to_years.join(', ')}. Pin to an older package version if needed.`);
24
+ }
25
+ // Compute MSC: round to nearest β‚±500, clamp to [min, max].
26
+ const { min, max, increment, regular_ss_cap } = table.msc;
27
+ let msc;
28
+ if (monthlyCompensation < min)
29
+ msc = min;
30
+ else if (monthlyCompensation >= max)
31
+ msc = max;
32
+ else
33
+ msc = Math.round(monthlyCompensation / increment) * increment;
34
+ // Split MSC into regular SS portion and MPF portion.
35
+ const regularMsc = Math.min(msc, regular_ss_cap);
36
+ const mpfMsc = Math.max(0, msc - regular_ss_cap);
37
+ const { employee_percent, employer_percent } = table.rate;
38
+ const regular = {
39
+ employee: round2(regularMsc * employee_percent),
40
+ employer: round2(regularMsc * employer_percent),
41
+ total: round2(regularMsc * (employee_percent + employer_percent)),
42
+ };
43
+ const mpf = {
44
+ employee: round2(mpfMsc * employee_percent),
45
+ employer: round2(mpfMsc * employer_percent),
46
+ total: round2(mpfMsc * (employee_percent + employer_percent)),
47
+ };
48
+ // EC: low or high based on MSC threshold.
49
+ const ec = msc <= table.ec.low_msc_threshold_inclusive ? table.ec.low_amount : table.ec.high_amount;
50
+ const employeeShare = round2(regular.employee + mpf.employee);
51
+ const employerShare = round2(regular.employer + mpf.employer + ec);
52
+ const total = round2(employeeShare + employerShare);
53
+ return { msc, regular, mpf, ec, employeeShare, employerShare, total };
54
+ }
@@ -0,0 +1,48 @@
1
+ import { type SSSContribution } from './sss.js';
2
+ import { type PhilHealthContribution } from './philhealth.js';
3
+ import { type PagIbigContribution } from './pagibig.js';
4
+ export interface NetTakeHome {
5
+ /** Gross monthly salary (input). */
6
+ gross: number;
7
+ /** Full SSS breakdown. */
8
+ sss: SSSContribution;
9
+ /** Full PhilHealth breakdown. */
10
+ philHealth: PhilHealthContribution;
11
+ /** Full Pag-IBIG breakdown. */
12
+ pagIbig: PagIbigContribution;
13
+ /** Sum of employee deductions across SSS, PhilHealth, and Pag-IBIG. */
14
+ totalDeductions: number;
15
+ /** Gross minus totalDeductions. Excludes BIR withholding tax β€” see notes. */
16
+ net: number;
17
+ }
18
+ export interface NetTakeHomeOptions {
19
+ year?: number;
20
+ }
21
+ /**
22
+ * Compute pre-tax net take-home: gross monthly salary minus mandatory
23
+ * SSS / PhilHealth / Pag-IBIG employee contributions.
24
+ *
25
+ * **DOES NOT INCLUDE BIR WITHHOLDING TAX.** Withholding requires taxable-income
26
+ * calculation (gross minus non-taxable allowances minus mandatory contributions),
27
+ * per-period derived tables, de minimis benefits handling, and year-end
28
+ * annualization. A simple `gross β†’ WT` calculator would be wrong in most real
29
+ * cases. WT is planned for v0.2 once the package has user feedback and
30
+ * accuracy validation.
31
+ *
32
+ * If you need an estimate including WT, compute taxable income manually and
33
+ * use your preferred tax engine.
34
+ *
35
+ * @param monthlySalary Gross monthly basic salary in pesos.
36
+ *
37
+ * @example
38
+ * netTakeHome(30000);
39
+ * // {
40
+ * // gross: 30000,
41
+ * // sss: { employeeShare: 1500, ... },
42
+ * // philHealth: { employee: 750, ... },
43
+ * // pagIbig: { employee: 200, ... },
44
+ * // totalDeductions: 2450,
45
+ * // net: 27550
46
+ * // }
47
+ */
48
+ export declare function netTakeHome(monthlySalary: number, opts?: NetTakeHomeOptions): NetTakeHome;
@@ -0,0 +1,51 @@
1
+ import { sssContribution } from './sss.js';
2
+ import { philHealthContribution } from './philhealth.js';
3
+ import { pagIbigContribution } from './pagibig.js';
4
+ function round2(n) {
5
+ return Math.round(n * 100) / 100;
6
+ }
7
+ /**
8
+ * Compute pre-tax net take-home: gross monthly salary minus mandatory
9
+ * SSS / PhilHealth / Pag-IBIG employee contributions.
10
+ *
11
+ * **DOES NOT INCLUDE BIR WITHHOLDING TAX.** Withholding requires taxable-income
12
+ * calculation (gross minus non-taxable allowances minus mandatory contributions),
13
+ * per-period derived tables, de minimis benefits handling, and year-end
14
+ * annualization. A simple `gross β†’ WT` calculator would be wrong in most real
15
+ * cases. WT is planned for v0.2 once the package has user feedback and
16
+ * accuracy validation.
17
+ *
18
+ * If you need an estimate including WT, compute taxable income manually and
19
+ * use your preferred tax engine.
20
+ *
21
+ * @param monthlySalary Gross monthly basic salary in pesos.
22
+ *
23
+ * @example
24
+ * netTakeHome(30000);
25
+ * // {
26
+ * // gross: 30000,
27
+ * // sss: { employeeShare: 1500, ... },
28
+ * // philHealth: { employee: 750, ... },
29
+ * // pagIbig: { employee: 200, ... },
30
+ * // totalDeductions: 2450,
31
+ * // net: 27550
32
+ * // }
33
+ */
34
+ export function netTakeHome(monthlySalary, opts = {}) {
35
+ if (!Number.isFinite(monthlySalary) || monthlySalary < 0) {
36
+ throw new TypeError('netTakeHome: monthlySalary must be a non-negative number');
37
+ }
38
+ const sss = sssContribution(monthlySalary, opts);
39
+ const philHealth = philHealthContribution(monthlySalary, opts);
40
+ const pagIbig = pagIbigContribution(monthlySalary, opts);
41
+ const totalDeductions = round2(sss.employeeShare + philHealth.employee + pagIbig.employee);
42
+ const net = round2(monthlySalary - totalDeductions);
43
+ return {
44
+ gross: monthlySalary,
45
+ sss,
46
+ philHealth,
47
+ pagIbig,
48
+ totalDeductions,
49
+ net,
50
+ };
51
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 13th-month pay calculators (PD 851).
3
+ *
4
+ * DOLE's official formula is: 13th-month = total basic salary earned in the
5
+ * calendar year Γ· 12. The "proration" for partial-year employees is captured
6
+ * by passing the actual total they earned, not by post-multiplying.
7
+ */
8
+ /**
9
+ * Compute 13th-month pay from total basic earnings.
10
+ * This is DOLE's canonical formula (PD 851).
11
+ *
12
+ * "Basic earnings" excludes allowances, overtime, holiday pay, night-shift
13
+ * differential, and other monetary benefits not part of base salary. It is
14
+ * the caller's responsibility to pass the correct figure.
15
+ *
16
+ * @param totalBasicEarnings Total basic salary actually earned in the
17
+ * computation window (typically Jan 1 – Dec 31). For a part-year employee,
18
+ * this is just the sum of their basic pay during the months they worked.
19
+ *
20
+ * @example
21
+ * thirteenthMonthPay(360000); // 30000 (full year @ 30k/month)
22
+ * thirteenthMonthPay(180000); // 15000 (6 months @ 30k/month)
23
+ */
24
+ export declare function thirteenthMonthPay(totalBasicEarnings: number): number;
25
+ /**
26
+ * Convenience for the common case: fixed monthly basic salary, possibly
27
+ * partial year. Equivalent to `thirteenthMonthPay(monthlyBasicSalary * monthsWorked)`.
28
+ *
29
+ * @param monthlyBasicSalary Monthly basic salary in pesos.
30
+ * @param monthsWorked Number of months worked during the year (default 12).
31
+ *
32
+ * @example
33
+ * thirteenthMonthFromMonthly(30000); // 30000 (full year)
34
+ * thirteenthMonthFromMonthly(30000, 6); // 15000 (6 months)
35
+ */
36
+ export declare function thirteenthMonthFromMonthly(monthlyBasicSalary: number, monthsWorked?: number): number;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 13th-month pay calculators (PD 851).
3
+ *
4
+ * DOLE's official formula is: 13th-month = total basic salary earned in the
5
+ * calendar year Γ· 12. The "proration" for partial-year employees is captured
6
+ * by passing the actual total they earned, not by post-multiplying.
7
+ */
8
+ /**
9
+ * Compute 13th-month pay from total basic earnings.
10
+ * This is DOLE's canonical formula (PD 851).
11
+ *
12
+ * "Basic earnings" excludes allowances, overtime, holiday pay, night-shift
13
+ * differential, and other monetary benefits not part of base salary. It is
14
+ * the caller's responsibility to pass the correct figure.
15
+ *
16
+ * @param totalBasicEarnings Total basic salary actually earned in the
17
+ * computation window (typically Jan 1 – Dec 31). For a part-year employee,
18
+ * this is just the sum of their basic pay during the months they worked.
19
+ *
20
+ * @example
21
+ * thirteenthMonthPay(360000); // 30000 (full year @ 30k/month)
22
+ * thirteenthMonthPay(180000); // 15000 (6 months @ 30k/month)
23
+ */
24
+ export function thirteenthMonthPay(totalBasicEarnings) {
25
+ if (!Number.isFinite(totalBasicEarnings) || totalBasicEarnings < 0) {
26
+ throw new TypeError('thirteenthMonthPay: totalBasicEarnings must be a non-negative number');
27
+ }
28
+ return Math.round((totalBasicEarnings / 12) * 100) / 100;
29
+ }
30
+ /**
31
+ * Convenience for the common case: fixed monthly basic salary, possibly
32
+ * partial year. Equivalent to `thirteenthMonthPay(monthlyBasicSalary * monthsWorked)`.
33
+ *
34
+ * @param monthlyBasicSalary Monthly basic salary in pesos.
35
+ * @param monthsWorked Number of months worked during the year (default 12).
36
+ *
37
+ * @example
38
+ * thirteenthMonthFromMonthly(30000); // 30000 (full year)
39
+ * thirteenthMonthFromMonthly(30000, 6); // 15000 (6 months)
40
+ */
41
+ export function thirteenthMonthFromMonthly(monthlyBasicSalary, monthsWorked = 12) {
42
+ if (!Number.isFinite(monthlyBasicSalary) || monthlyBasicSalary < 0) {
43
+ throw new TypeError('thirteenthMonthFromMonthly: monthlyBasicSalary must be a non-negative number');
44
+ }
45
+ if (!Number.isFinite(monthsWorked) || monthsWorked < 0 || monthsWorked > 12) {
46
+ throw new RangeError('thirteenthMonthFromMonthly: monthsWorked must be between 0 and 12');
47
+ }
48
+ return thirteenthMonthPay(monthlyBasicSalary * monthsWorked);
49
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@ph-dev-utils/payroll",
3
+ "version": "0.1.0",
4
+ "description": "Filipino payroll calculators β€” SSS, PhilHealth, Pag-IBIG contributions; 13th month pay; net take-home. Versioned contribution tables per effective year.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "data",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
25
+ },
26
+ "keywords": [
27
+ "philippines",
28
+ "filipino",
29
+ "payroll",
30
+ "sss",
31
+ "philhealth",
32
+ "pagibig",
33
+ "13th-month",
34
+ "contribution",
35
+ "calculator"
36
+ ],
37
+ "devDependencies": {
38
+ "typescript": "^5.4.0",
39
+ "vitest": "^1.5.0"
40
+ }
41
+ }