@ph-dev-utils/payroll 0.1.0 β†’ 0.2.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 CHANGED
@@ -4,11 +4,11 @@
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kon2raya24/ph-payroll/blob/main/LICENSE)
5
5
  [![Made in PH](https://img.shields.io/badge/made%20in-πŸ‡΅πŸ‡­%20Philippines-0038A8)](https://github.com/kon2raya24)
6
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.
7
+ Filipino payroll calculators for JavaScript / TypeScript β€” SSS, PhilHealth, Pag-IBIG monthly contributions, 13th-month pay, BIR monthly withholding tax (TRAIN), and pre-tax / post-tax net take-home. Tables versioned by effective date.
8
8
 
9
9
  ## ⚠️ READ FIRST
10
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).
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 WT in v0.2 is monthly-only** and operates on **taxable income**, not raw gross. De minimis caps and the β‚±90k 13th-month annual exemption are caller-handled in v0.2.
12
12
 
13
13
  Full disclaimer: [project README](https://github.com/kon2raya24/ph-payroll#-read-first--accuracy-disclaimer).
14
14
 
@@ -26,6 +26,7 @@ Requires Node 20+.
26
26
  import { netTakeHome } from '@ph-dev-utils/payroll';
27
27
 
28
28
  netTakeHome(30000);
29
+ // pre-tax (v0.1 shape β€” unchanged):
29
30
  // {
30
31
  // gross: 30000,
31
32
  // sss: { msc: 30000, employeeShare: 1500, employerShare: 3030, ... },
@@ -34,6 +35,9 @@ netTakeHome(30000);
34
35
  // totalDeductions: 2450,
35
36
  // net: 27550
36
37
  // }
38
+
39
+ netTakeHome(30000, { includeWT: true });
40
+ // adds: taxableIncome: 27550, withholdingTax: 1007.55, netAfterTax: 26542.45
37
41
  ```
38
42
 
39
43
  ## API Reference
@@ -151,14 +155,84 @@ thirteenthMonthFromMonthly(30000, 1); // 2500
151
155
 
152
156
  ---
153
157
 
158
+ ### `withholdingTaxMonthly(taxableIncome, opts?)` *(v0.2+)*
159
+
160
+ Source: **BIR RR 11-2018** monthly table (TRAIN Law, second-phase rates effective 2023-01-01).
161
+
162
+ ```ts
163
+ function withholdingTaxMonthly(
164
+ taxableIncome: number,
165
+ opts?: { year?: number }
166
+ ): { wt: number; bracket: 1|2|3|4|5|6; marginalRate: number }
167
+ ```
168
+
169
+ **Input is taxable income, not gross.** To derive taxable income from gross, use [`taxableIncomeMonthly`](#taxableincomemonthlygross-opts-v02) (or compute manually as `gross βˆ’ mandatories βˆ’ nonTaxableAllowances`).
170
+
171
+ **BIR monthly brackets (TRAIN 2023+):**
172
+
173
+ | Bracket | Monthly taxable income | Tax |
174
+ | --- | --- | --- |
175
+ | 1 | ≀ β‚±20,833 | 0 |
176
+ | 2 | β‚±20,833 – β‚±33,333 | 15% of excess over β‚±20,833 |
177
+ | 3 | β‚±33,333 – β‚±66,667 | β‚±1,875 + 20% of excess over β‚±33,333 |
178
+ | 4 | β‚±66,667 – β‚±166,667 | β‚±8,541.80 + 25% of excess over β‚±66,667 |
179
+ | 5 | β‚±166,667 – β‚±666,667 | β‚±33,541.80 + 30% of excess over β‚±166,667 |
180
+ | 6 | > β‚±666,667 | β‚±183,541.80 + 35% of excess over β‚±666,667 |
181
+
182
+ ```ts
183
+ withholdingTaxMonthly(15000); // { wt: 0, bracket: 1, marginalRate: 0 }
184
+ withholdingTaxMonthly(25000); // { wt: 625.05, bracket: 2, marginalRate: 0.15 }
185
+ withholdingTaxMonthly(50000); // { wt: 5208.4, bracket: 3, marginalRate: 0.20 }
186
+ withholdingTaxMonthly(100000); // { wt: 16875.05, bracket: 4, marginalRate: 0.25 }
187
+ withholdingTaxMonthly(200000); // { wt: 43541.7, bracket: 5, marginalRate: 0.30 }
188
+ withholdingTaxMonthly(1000000); // { wt: 300208.35, bracket: 6, marginalRate: 0.35 }
189
+ ```
190
+
191
+ ---
192
+
193
+ ### `taxableIncomeMonthly(gross, opts?)` *(v0.2+)*
194
+
195
+ Derive monthly taxable income from gross. Formula:
196
+
197
+ ```
198
+ taxableIncome = gross βˆ’ mandatoryDeductions βˆ’ nonTaxableAllowances
199
+ ```
200
+
201
+ ```ts
202
+ function taxableIncomeMonthly(
203
+ gross: number,
204
+ opts?: {
205
+ year?: number;
206
+ mandatoryDeductions?: number; // override SSS+PH+Pag-IBIG; default: auto-computed
207
+ nonTaxableAllowances?: number; // de minimis within caps + tax-exempt allowances; default: 0
208
+ }
209
+ ): number
210
+ ```
211
+
212
+ Caller-handled (not done here):
213
+ - **De minimis caps** β€” pass only the non-taxable *portion* of each benefit. Rice subsidy is exempt up to β‚±2,000/mo, uniform β‚±6,000/yr, medical β‚±10,000/yr (employee), etc. If a benefit exceeds its cap, the excess is taxable and should NOT appear in `nonTaxableAllowances`.
214
+ - **13th-month / bonus β‚±90k annual exemption** β€” applies at year level, not month. Excess goes in `gross` for the month received.
215
+
216
+ ```ts
217
+ taxableIncomeMonthly(30000); // 27550 (auto: gross βˆ’ 2,450 mandatories βˆ’ 0)
218
+ taxableIncomeMonthly(30000, { nonTaxableAllowances: 2000 }); // 25550
219
+ taxableIncomeMonthly(50000, { mandatoryDeductions: 3200 }); // 46800
220
+ ```
221
+
222
+ ---
223
+
154
224
  ### `netTakeHome(monthlySalary, opts?)`
155
225
 
156
- Pre-tax net take-home: gross salary minus mandatory SSS / PhilHealth / Pag-IBIG employee contributions.
226
+ Net take-home: gross salary minus mandatory SSS / PhilHealth / Pag-IBIG employee contributions, and (optionally, v0.2+) BIR withholding tax.
157
227
 
158
228
  ```ts
159
229
  function netTakeHome(
160
230
  monthlySalary: number,
161
- opts?: { year?: number }
231
+ opts?: {
232
+ year?: number;
233
+ includeWT?: boolean; // v0.2+: also compute WT and netAfterTax
234
+ nonTaxableAllowances?: number; // v0.2+: only used when includeWT is true
235
+ }
162
236
  ): NetTakeHome
163
237
 
164
238
  interface NetTakeHome {
@@ -166,17 +240,29 @@ interface NetTakeHome {
166
240
  sss: SSSContribution;
167
241
  philHealth: PhilHealthContribution;
168
242
  pagIbig: PagIbigContribution;
169
- totalDeductions: number; // sum of employee shares
170
- net: number; // gross - totalDeductions
243
+ totalDeductions: number; // sum of employee shares
244
+ net: number; // gross - totalDeductions (pre-tax)
245
+ taxableIncome?: number; // populated only when includeWT
246
+ withholdingTax?: number; // populated only when includeWT
247
+ netAfterTax?: number; // populated only when includeWT
171
248
  }
172
249
  ```
173
250
 
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.).
251
+ By default (no `includeWT`), this preserves the v0.1 pre-tax shape. The `includeWT: true` opt-in adds three fields without removing any.
175
252
 
176
253
  ```ts
177
- netTakeHome(30000).net; // 27550
178
- netTakeHome(8000).net; // 7190 (low-earner with floor PhilHealth + EC)
179
- netTakeHome(150000).net; // 145550 (capped contributions)
254
+ // v0.1 shape (default) β€” pre-tax:
255
+ netTakeHome(30000).net; // 27550
256
+ netTakeHome(8000).net; // 7190
257
+ netTakeHome(150000).net; // 145550
258
+
259
+ // v0.2+ with WT:
260
+ netTakeHome(30000, { includeWT: true }).withholdingTax; // 1007.55
261
+ netTakeHome(30000, { includeWT: true }).netAfterTax; // 26542.45
262
+
263
+ // v0.2+ with non-taxable allowances:
264
+ netTakeHome(30000, { includeWT: true, nonTaxableAllowances: 2000 }).withholdingTax;
265
+ // 707.55 (lower because TI dropped from 27,550 β†’ 25,550)
180
266
  ```
181
267
 
182
268
  ---
@@ -0,0 +1,28 @@
1
+ {
2
+ "_meta": {
3
+ "effective_from": "2023-01-01",
4
+ "effective_until": null,
5
+ "applies_to_years": [2023, 2024, 2025, 2026],
6
+ "period": "monthly",
7
+ "source": "BIR Revenue Regulations No. 11-2018 (as amended by TRAIN Law) β€” Monthly Compensation Level (MCL) table, Annex E",
8
+ "source_url": "https://www.bir.gov.ph/index.php/tax-information/withholding-tax.html",
9
+ "authority": "Republic Act No. 10963 (Tax Reform for Acceleration and Inclusion / TRAIN Law), as further amended by RA 11534 (CREATE Law) β€” individual income tax brackets effective January 1, 2023 onwards (second phase of TRAIN rate reductions)",
10
+ "verified_on": "2026-05-20",
11
+ "notes": [
12
+ "Withholding tax is applied to TAXABLE INCOME, not gross compensation.",
13
+ "Taxable income = gross compensation βˆ’ mandatory employee contributions (SSS, PhilHealth, Pag-IBIG) βˆ’ non-taxable allowances (de minimis benefits within caps).",
14
+ "13th-month pay and other bonuses are non-taxable up to β‚±90,000 per year (excess is taxable in the year of receipt); not applied at monthly level.",
15
+ "Bracket bases (1875.00, 8541.80, 33541.80, 183541.80) are the BIR-published rounded monthly values derived from annualized cumulative tax (annual base Γ· 12).",
16
+ "This table is for MONTHLY payroll periods only. Semi-monthly, weekly, and daily payrolls use separate BIR-published tables (planned for v0.3)."
17
+ ]
18
+ },
19
+
20
+ "brackets": [
21
+ { "bracket": 1, "over": 0, "not_over": 20833, "base": 0.00, "rate_over_min": 0.00 },
22
+ { "bracket": 2, "over": 20833, "not_over": 33333, "base": 0.00, "rate_over_min": 0.15 },
23
+ { "bracket": 3, "over": 33333, "not_over": 66667, "base": 1875.00, "rate_over_min": 0.20 },
24
+ { "bracket": 4, "over": 66667, "not_over": 166667, "base": 8541.80, "rate_over_min": 0.25 },
25
+ { "bracket": 5, "over": 166667, "not_over": 666667, "base": 33541.80, "rate_over_min": 0.30 },
26
+ { "bracket": 6, "over": 666667, "not_over": null, "base": 183541.80, "rate_over_min": 0.35 }
27
+ ]
28
+ }
package/dist/index.d.ts CHANGED
@@ -5,5 +5,7 @@ export type { PhilHealthContribution, PhilHealthOptions } from './philhealth.js'
5
5
  export { pagIbigContribution } from './pagibig.js';
6
6
  export type { PagIbigContribution, PagIbigOptions } from './pagibig.js';
7
7
  export { thirteenthMonthPay, thirteenthMonthFromMonthly } from './thirteenth-month.js';
8
+ export { withholdingTaxMonthly, taxableIncomeMonthly } from './withholding-tax.js';
9
+ export type { WithholdingTaxResult, TaxableIncomeOptions } from './withholding-tax.js';
8
10
  export { netTakeHome } from './take-home.js';
9
11
  export type { NetTakeHome, NetTakeHomeOptions } from './take-home.js';
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export { sssContribution } from './sss.js';
2
2
  export { philHealthContribution } from './philhealth.js';
3
3
  export { pagIbigContribution } from './pagibig.js';
4
4
  export { thirteenthMonthPay, thirteenthMonthFromMonthly } from './thirteenth-month.js';
5
+ export { withholdingTaxMonthly, taxableIncomeMonthly } from './withholding-tax.js';
5
6
  export { netTakeHome } from './take-home.js';
@@ -12,37 +12,42 @@ export interface NetTakeHome {
12
12
  pagIbig: PagIbigContribution;
13
13
  /** Sum of employee deductions across SSS, PhilHealth, and Pag-IBIG. */
14
14
  totalDeductions: number;
15
- /** Gross minus totalDeductions. Excludes BIR withholding tax β€” see notes. */
15
+ /** Gross minus totalDeductions. Pre-tax β€” see `netAfterTax` for post-WT. */
16
16
  net: number;
17
+ /** Monthly taxable income (only present when `includeWT: true`). */
18
+ taxableIncome?: number;
19
+ /** BIR monthly withholding tax (only present when `includeWT: true`). */
20
+ withholdingTax?: number;
21
+ /** Net minus withholdingTax (only present when `includeWT: true`). */
22
+ netAfterTax?: number;
17
23
  }
18
24
  export interface NetTakeHomeOptions {
19
25
  year?: number;
26
+ /**
27
+ * If true, also compute BIR monthly withholding tax (v0.2+) and populate
28
+ * `taxableIncome`, `withholdingTax`, and `netAfterTax` on the result.
29
+ * Default: false (preserves v0.1 pre-tax shape).
30
+ */
31
+ includeWT?: boolean;
32
+ /**
33
+ * Total non-taxable allowances for the month. Used only when `includeWT: true`.
34
+ * Caller is responsible for capping per BIR de minimis rules.
35
+ */
36
+ nonTaxableAllowances?: number;
20
37
  }
21
38
  /**
22
- * Compute pre-tax net take-home: gross monthly salary minus mandatory
23
- * SSS / PhilHealth / Pag-IBIG employee contributions.
39
+ * Compute net take-home: gross monthly salary minus mandatory SSS / PhilHealth /
40
+ * Pag-IBIG employee contributions, and (optionally, v0.2+) BIR withholding tax.
24
41
  *
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.
42
+ * By default, returns **pre-tax** net (v0.1 shape). Pass `{ includeWT: true }` to
43
+ * also compute monthly WT and `netAfterTax`. WT is computed off taxable income,
44
+ * which is derived as `gross βˆ’ mandatories βˆ’ nonTaxableAllowances`.
36
45
  *
37
46
  * @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
+ * netTakeHome(30000); // pre-tax, v0.1 shape
48
+ * // { gross: 30000, ..., totalDeductions: 2450, net: 27550 }
49
+ *
50
+ * netTakeHome(30000, { includeWT: true }); // adds WT fields
51
+ * // { ..., net: 27550, taxableIncome: 27550, withholdingTax: 1007.55, netAfterTax: 26542.45 }
47
52
  */
48
53
  export declare function netTakeHome(monthlySalary: number, opts?: NetTakeHomeOptions): NetTakeHome;
package/dist/take-home.js CHANGED
@@ -1,35 +1,24 @@
1
1
  import { sssContribution } from './sss.js';
2
2
  import { philHealthContribution } from './philhealth.js';
3
3
  import { pagIbigContribution } from './pagibig.js';
4
+ import { withholdingTaxMonthly, taxableIncomeMonthly } from './withholding-tax.js';
4
5
  function round2(n) {
5
6
  return Math.round(n * 100) / 100;
6
7
  }
7
8
  /**
8
- * Compute pre-tax net take-home: gross monthly salary minus mandatory
9
- * SSS / PhilHealth / Pag-IBIG employee contributions.
9
+ * Compute net take-home: gross monthly salary minus mandatory SSS / PhilHealth /
10
+ * Pag-IBIG employee contributions, and (optionally, v0.2+) BIR withholding tax.
10
11
  *
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.
12
+ * By default, returns **pre-tax** net (v0.1 shape). Pass `{ includeWT: true }` to
13
+ * also compute monthly WT and `netAfterTax`. WT is computed off taxable income,
14
+ * which is derived as `gross βˆ’ mandatories βˆ’ nonTaxableAllowances`.
22
15
  *
23
16
  * @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
- * // }
17
+ * netTakeHome(30000); // pre-tax, v0.1 shape
18
+ * // { gross: 30000, ..., totalDeductions: 2450, net: 27550 }
19
+ *
20
+ * netTakeHome(30000, { includeWT: true }); // adds WT fields
21
+ * // { ..., net: 27550, taxableIncome: 27550, withholdingTax: 1007.55, netAfterTax: 26542.45 }
33
22
  */
34
23
  export function netTakeHome(monthlySalary, opts = {}) {
35
24
  if (!Number.isFinite(monthlySalary) || monthlySalary < 0) {
@@ -40,7 +29,7 @@ export function netTakeHome(monthlySalary, opts = {}) {
40
29
  const pagIbig = pagIbigContribution(monthlySalary, opts);
41
30
  const totalDeductions = round2(sss.employeeShare + philHealth.employee + pagIbig.employee);
42
31
  const net = round2(monthlySalary - totalDeductions);
43
- return {
32
+ const result = {
44
33
  gross: monthlySalary,
45
34
  sss,
46
35
  philHealth,
@@ -48,4 +37,16 @@ export function netTakeHome(monthlySalary, opts = {}) {
48
37
  totalDeductions,
49
38
  net,
50
39
  };
40
+ if (opts.includeWT) {
41
+ const taxableIncome = taxableIncomeMonthly(monthlySalary, {
42
+ year: opts.year,
43
+ mandatoryDeductions: totalDeductions,
44
+ nonTaxableAllowances: opts.nonTaxableAllowances,
45
+ });
46
+ const { wt } = withholdingTaxMonthly(taxableIncome, { year: opts.year });
47
+ result.taxableIncome = taxableIncome;
48
+ result.withholdingTax = wt;
49
+ result.netAfterTax = round2(net - wt);
50
+ }
51
+ return result;
51
52
  }
@@ -0,0 +1,60 @@
1
+ export interface WithholdingTaxResult {
2
+ /** Computed withholding tax for the month. */
3
+ wt: number;
4
+ /** Bracket number (1–6) that the taxable income fell into. */
5
+ bracket: 1 | 2 | 3 | 4 | 5 | 6;
6
+ /** Marginal rate applied to the portion above the bracket floor. */
7
+ marginalRate: number;
8
+ }
9
+ export interface TaxableIncomeOptions {
10
+ year?: number;
11
+ /**
12
+ * Override mandatory deductions (SSS+PhilHealth+Pag-IBIG employee shares).
13
+ * If omitted, computed automatically from the gross via v0.1 contribution functions.
14
+ */
15
+ mandatoryDeductions?: number;
16
+ /**
17
+ * Total non-taxable allowances for the month (de minimis benefits within their caps,
18
+ * other tax-exempt allowances). Caller is responsible for capping per BIR rules β€” this
19
+ * function trusts the value as given.
20
+ */
21
+ nonTaxableAllowances?: number;
22
+ }
23
+ /**
24
+ * Compute BIR withholding tax for a given **monthly taxable income**.
25
+ *
26
+ * Source: BIR RR 11-2018 monthly table (TRAIN Law, second phase of rate reductions
27
+ * effective 2023-01-01).
28
+ *
29
+ * **Important:** the input is *taxable income*, not gross compensation. To derive
30
+ * taxable income from gross, use {@link taxableIncomeMonthly}.
31
+ *
32
+ * @example
33
+ * withholdingTaxMonthly(25000);
34
+ * // { wt: 625, bracket: 2, marginalRate: 0.15 }
35
+ * // (15% Γ— (25,000 βˆ’ 20,833) = 625.05; rounded)
36
+ */
37
+ export declare function withholdingTaxMonthly(taxableIncome: number, opts?: {
38
+ year?: number;
39
+ }): WithholdingTaxResult;
40
+ /**
41
+ * Derive monthly taxable income from gross compensation.
42
+ *
43
+ * Formula: `gross βˆ’ mandatoryDeductions βˆ’ nonTaxableAllowances`
44
+ *
45
+ * If `mandatoryDeductions` is omitted, it is auto-computed as the sum of SSS,
46
+ * PhilHealth, and Pag-IBIG employee shares for the given gross (using v0.1
47
+ * contribution functions).
48
+ *
49
+ * **Not handled here:** 13th-month + bonus β‚±90,000 annual exemption (that's an
50
+ * annual concern, not monthly). De minimis benefit caps are the caller's
51
+ * responsibility β€” pass only the *non-taxable portion* in `nonTaxableAllowances`.
52
+ *
53
+ * @example
54
+ * taxableIncomeMonthly(30000);
55
+ * // 27,550 (gross βˆ’ auto-computed 2,450 mandatories βˆ’ 0 allowances)
56
+ *
57
+ * taxableIncomeMonthly(50000, { nonTaxableAllowances: 2000 });
58
+ * // 50,000 βˆ’ mandatories(50k) βˆ’ 2,000
59
+ */
60
+ export declare function taxableIncomeMonthly(gross: number, opts?: TaxableIncomeOptions): number;
@@ -0,0 +1,88 @@
1
+ import table from '../data/bir-wt-monthly-train-2023.json' with { type: 'json' };
2
+ import { sssContribution } from './sss.js';
3
+ import { philHealthContribution } from './philhealth.js';
4
+ import { pagIbigContribution } from './pagibig.js';
5
+ function round2(n) {
6
+ return Math.round(n * 100) / 100;
7
+ }
8
+ function assertYearSupported(year) {
9
+ if (year === undefined)
10
+ return;
11
+ if (!table._meta.applies_to_years.includes(year)) {
12
+ throw new RangeError(`withholding-tax: no BIR monthly WT table available for year ${year}. Available: ${table._meta.applies_to_years.join(', ')}. Pin to an older package version if needed.`);
13
+ }
14
+ }
15
+ /**
16
+ * Compute BIR withholding tax for a given **monthly taxable income**.
17
+ *
18
+ * Source: BIR RR 11-2018 monthly table (TRAIN Law, second phase of rate reductions
19
+ * effective 2023-01-01).
20
+ *
21
+ * **Important:** the input is *taxable income*, not gross compensation. To derive
22
+ * taxable income from gross, use {@link taxableIncomeMonthly}.
23
+ *
24
+ * @example
25
+ * withholdingTaxMonthly(25000);
26
+ * // { wt: 625, bracket: 2, marginalRate: 0.15 }
27
+ * // (15% Γ— (25,000 βˆ’ 20,833) = 625.05; rounded)
28
+ */
29
+ export function withholdingTaxMonthly(taxableIncome, opts = {}) {
30
+ if (!Number.isFinite(taxableIncome) || taxableIncome < 0) {
31
+ throw new TypeError('withholdingTaxMonthly: taxableIncome must be a non-negative number');
32
+ }
33
+ assertYearSupported(opts.year);
34
+ const brackets = table.brackets;
35
+ let chosen = brackets[0];
36
+ for (const b of brackets) {
37
+ if (taxableIncome > b.over)
38
+ chosen = b;
39
+ else
40
+ break;
41
+ }
42
+ const wt = round2(chosen.base + chosen.rate_over_min * (taxableIncome - chosen.over));
43
+ return {
44
+ wt,
45
+ bracket: chosen.bracket,
46
+ marginalRate: chosen.rate_over_min,
47
+ };
48
+ }
49
+ /**
50
+ * Derive monthly taxable income from gross compensation.
51
+ *
52
+ * Formula: `gross βˆ’ mandatoryDeductions βˆ’ nonTaxableAllowances`
53
+ *
54
+ * If `mandatoryDeductions` is omitted, it is auto-computed as the sum of SSS,
55
+ * PhilHealth, and Pag-IBIG employee shares for the given gross (using v0.1
56
+ * contribution functions).
57
+ *
58
+ * **Not handled here:** 13th-month + bonus β‚±90,000 annual exemption (that's an
59
+ * annual concern, not monthly). De minimis benefit caps are the caller's
60
+ * responsibility β€” pass only the *non-taxable portion* in `nonTaxableAllowances`.
61
+ *
62
+ * @example
63
+ * taxableIncomeMonthly(30000);
64
+ * // 27,550 (gross βˆ’ auto-computed 2,450 mandatories βˆ’ 0 allowances)
65
+ *
66
+ * taxableIncomeMonthly(50000, { nonTaxableAllowances: 2000 });
67
+ * // 50,000 βˆ’ mandatories(50k) βˆ’ 2,000
68
+ */
69
+ export function taxableIncomeMonthly(gross, opts = {}) {
70
+ if (!Number.isFinite(gross) || gross < 0) {
71
+ throw new TypeError('taxableIncomeMonthly: gross must be a non-negative number');
72
+ }
73
+ let mandatory = opts.mandatoryDeductions;
74
+ if (mandatory === undefined) {
75
+ const sss = sssContribution(gross, { year: opts.year });
76
+ const ph = philHealthContribution(gross, { year: opts.year });
77
+ const pi = pagIbigContribution(gross, { year: opts.year });
78
+ mandatory = sss.employeeShare + ph.employee + pi.employee;
79
+ }
80
+ if (!Number.isFinite(mandatory) || mandatory < 0) {
81
+ throw new TypeError('taxableIncomeMonthly: mandatoryDeductions must be a non-negative number');
82
+ }
83
+ const nta = opts.nonTaxableAllowances ?? 0;
84
+ if (!Number.isFinite(nta) || nta < 0) {
85
+ throw new TypeError('taxableIncomeMonthly: nonTaxableAllowances must be a non-negative number');
86
+ }
87
+ return round2(Math.max(0, gross - mandatory - nta));
88
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
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.",
3
+ "version": "0.2.0",
4
+ "description": "Filipino payroll calculators β€” SSS, PhilHealth, Pag-IBIG contributions; 13th month pay; BIR monthly withholding tax (TRAIN); net take-home. Versioned contribution tables per effective year.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -30,6 +30,9 @@
30
30
  "sss",
31
31
  "philhealth",
32
32
  "pagibig",
33
+ "bir",
34
+ "withholding-tax",
35
+ "train-law",
33
36
  "13th-month",
34
37
  "contribution",
35
38
  "calculator"