@seethruhead/cra-payroll 0.5.2 → 0.6.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.
@@ -79630,7 +79630,7 @@ import { existsSync as existsSync4, renameSync, unlinkSync, chmodSync } from "fs
79630
79630
  // package.json
79631
79631
  var package_default = {
79632
79632
  name: "@seethruhead/cra-payroll",
79633
- version: "0.5.2",
79633
+ version: "0.6.0",
79634
79634
  description: "Calculate Canadian payroll deductions using CRA's Payroll Deductions Online Calculator",
79635
79635
  type: "module",
79636
79636
  bin: {
@@ -79644,7 +79644,7 @@ var package_default = {
79644
79644
  build: "bun build --compile src/cli.ts --outfile cra-payroll --external electron",
79645
79645
  "build:npm": "bun build src/cli.ts --outfile dist/cra-payroll.js --target=node",
79646
79646
  release: "bash release.sh",
79647
- test: "bun test src/unit.test.ts src/cache.test.ts src/monthly.test.ts src/views.test.ts",
79647
+ test: "bun test src/unit.test.ts src/cache.test.ts src/monthly.test.ts src/views.test.ts src/brackets.test.ts",
79648
79648
  "test:integration": "bun test src/integration.test.ts --timeout 120000 --max-concurrency 1",
79649
79649
  "test:all": "bun test --timeout 120000 --max-concurrency 1"
79650
79650
  },
@@ -79826,6 +79826,55 @@ ${line("═", W)}
79826
79826
  Net Pay: $${f(r2.net)}${when(totalEmployeeRrsp, ` (after RRSP): $${f(r2.net - totalEmployeeRrsp)}`)}`;
79827
79827
  };
79828
79828
 
79829
+ // src/brackets.ts
79830
+ var brackets = (...bands) => (income) => (bands.find(([upTo]) => income <= upTo) ?? bands[bands.length - 1])[1];
79831
+ var cumulativeTax = (bands) => (income) => bands.reduce(({ tax, prev }, [cap, rate]) => ({
79832
+ tax: income > prev ? tax + (Math.min(income, cap) - prev) * rate : tax,
79833
+ prev: cap
79834
+ }), { tax: 0, prev: 0 }).tax;
79835
+ var withSurtax = (baseFn, taxFn, threshold1, threshold2) => (income) => {
79836
+ const rate = baseFn(income);
79837
+ const tax = taxFn(income);
79838
+ let mult = 1;
79839
+ if (tax > threshold1)
79840
+ mult += 0.2;
79841
+ if (tax > threshold2)
79842
+ mult += 0.36;
79843
+ return rate * mult;
79844
+ };
79845
+ var on2025bands = [[52886, 0.0505], [105775, 0.0915], [150000, 0.1116], [220000, 0.1216], [Infinity, 0.1316]];
79846
+ var on2026bands = [[54000, 0.0505], [108000, 0.0915], [150000, 0.1116], [220000, 0.1216], [Infinity, 0.1316]];
79847
+ var RATES = {
79848
+ 2025: {
79849
+ federal: brackets([57375, 0.15], [114750, 0.205], [158468, 0.26], [220000, 0.29], [Infinity, 0.33]),
79850
+ provincial: {
79851
+ Ontario: withSurtax(brackets(...on2025bands), cumulativeTax(on2025bands), 4991, 6387),
79852
+ Alberta: brackets([148269, 0.1], [177922, 0.12], [237230, 0.13], [355845, 0.14], [Infinity, 0.15]),
79853
+ "British Columbia": brackets([47937, 0.0506], [95875, 0.077], [110076, 0.105], [133664, 0.1229], [181232, 0.147], [252752, 0.168], [Infinity, 0.205])
79854
+ }
79855
+ },
79856
+ 2026: {
79857
+ federal: brackets([59000, 0.15], [118000, 0.205], [163000, 0.26], [227000, 0.29], [Infinity, 0.33]),
79858
+ provincial: {
79859
+ Ontario: withSurtax(brackets(...on2026bands), cumulativeTax(on2026bands), 5100, 6525),
79860
+ Alberta: brackets([152000, 0.1], [182000, 0.12], [243000, 0.13], [365000, 0.14], [Infinity, 0.15]),
79861
+ "British Columbia": brackets([49000, 0.0506], [98000, 0.077], [113000, 0.105], [137000, 0.1229], [186000, 0.147], [259000, 0.168], [Infinity, 0.205])
79862
+ }
79863
+ }
79864
+ };
79865
+ var supportedYears = () => Object.keys(RATES).map(Number);
79866
+ var marginalRate = (year, province, taxableIncome) => {
79867
+ const yearRates = RATES[year];
79868
+ if (!yearRates)
79869
+ return $err(`No tax bracket data for ${year}. Supported years: ${supportedYears().join(", ")}`);
79870
+ const provFn = yearRates.provincial[province];
79871
+ if (!provFn)
79872
+ return $err(`No provincial bracket data for ${province} in ${year}. Supported: ${Object.keys(yearRates.provincial).join(", ")}`);
79873
+ const fed = yearRates.federal(taxableIncome);
79874
+ const prov = provFn(taxableIncome);
79875
+ return $ok({ federal: fed, provincial: prov, combined: fed + prov });
79876
+ };
79877
+
79829
79878
  // src/views/table.ts
79830
79879
  var col = (v, w) => typeof v === "number" ? money(v).padStart(w) : v.padStart(w);
79831
79880
  var renderRow = (r2) => `${r2.label} │ ${col(r2.gross, 10)} │ ${col(r2.fedTax, 10)} │ ${col(r2.provTax, 10)} │ ${col(r2.cpp, 10)} │ ${col(r2.cpp2, 6)} │ ${col(r2.ei, 10)} │ ${col(r2.netPay, 10)}${r2.rrspYou !== undefined ? ` │ ${col(r2.rrspYou, 10)} │ ${col(r2.rrspEr, 10)} │ ${col(r2.takeHome, 10)}` : ""} │ ${col(r2.cumCppEi, 10)}${r2.suffix ?? ""}`;
@@ -79871,7 +79920,7 @@ var totalsRow = (totals, rrsp) => ({
79871
79920
  ...rrspFields(rrsp, { rrspYou: totalEmployeeRrspTotals(totals), rrspEr: totals.rrspEmployer, takeHome: totals.netPay - totalEmployeeRrspTotals(totals) }),
79872
79921
  cumCppEi: ""
79873
79922
  });
79874
- var renderTable = (yearly, periodsPerYear, year = 2026) => t3(yearly, (ctx) => ({ ...ctx, rrsp: showRrsp(ctx.totals) }), (ctx) => ({ ...ctx, header: renderRow(headerRow(ctx.rrsp)) }), (ctx) => ({ ...ctx, bodyRows: ctx.rows.map((r2) => renderRow(toRow(r2, ctx.rows[0], ctx.rrsp))) }), (ctx) => ({ ...ctx, totalsStr: renderRow(totalsRow(ctx.totals, ctx.rrsp)), W: ctx.header.length }), (ctx) => {
79923
+ var renderTable = (yearly, periodsPerYear, year = 2026, province = "Ontario", annualSalary = 0, rrspPercent = 0) => t3(yearly, (ctx) => ({ ...ctx, rrsp: showRrsp(ctx.totals) }), (ctx) => ({ ...ctx, header: renderRow(headerRow(ctx.rrsp)) }), (ctx) => ({ ...ctx, bodyRows: ctx.rows.map((r2) => renderRow(toRow(r2, ctx.rows[0], ctx.rrsp))) }), (ctx) => ({ ...ctx, totalsStr: renderRow(totalsRow(ctx.totals, ctx.rrsp)), W: ctx.header.length }), (ctx) => {
79875
79924
  const t8 = ctx.totals;
79876
79925
  const empTotal = totalEmployeeRrspTotals(t8);
79877
79926
  const rrspSummary = !ctx.rrsp ? "" : `
@@ -79879,6 +79928,9 @@ var renderTable = (yearly, periodsPerYear, year = 2026) => t3(yearly, (ctx) => (
79879
79928
  RRSP Er: $${money(t8.rrspEmployer)}/yr ($${money(t8.rrspEmployer / periodsPerYear)}/period)
79880
79929
  RRSP Total (You + Er): $${money(empTotal + t8.rrspEmployer)}/yr`;
79881
79930
  const taxRate = t8.grossIncome > 0 ? t8.totalDeductions / t8.grossIncome * 100 : 0;
79931
+ const taxableIncome = annualSalary * (1 - rrspPercent / 100);
79932
+ const mr = marginalRate(year, province, taxableIncome);
79933
+ const marginalLine = mr.isOk() ? ` Marginal tax rate: ${pct(mr.value.combined * 100)} (fed ${pct(mr.value.federal * 100)} + prov ${pct(mr.value.provincial * 100)})` : "";
79882
79934
  return `Per-Paycheck Table (${year})
79883
79935
  ${line("═", ctx.W)}
79884
79936
  ${ctx.header}
@@ -79888,7 +79940,8 @@ ${ctx.bodyRows.join(`
79888
79940
  ${line("─", ctx.W)}
79889
79941
  ${ctx.totalsStr}
79890
79942
  ${line("═", ctx.W)}
79891
- Effective tax rate: ${pct(taxRate)}${rrspSummary}`;
79943
+ Effective tax rate: ${pct(taxRate)}${marginalLine ? `
79944
+ ${marginalLine}` : ""}${rrspSummary}`;
79892
79945
  });
79893
79946
 
79894
79947
  // src/views/monthlyTable.ts
@@ -79931,7 +79984,7 @@ var totalsRow2 = (totals, rrsp) => ({
79931
79984
  netPay: totals.netPay,
79932
79985
  ...rrspFields2(rrsp, { rrspYou: totalEmployeeRrspTotals2(totals), rrspEr: totals.rrspEmployer, takeHome: totals.netPay - totalEmployeeRrspTotals2(totals) })
79933
79986
  });
79934
- var renderMonthlyTable = (monthly, year = 2026) => t3(monthly, (ctx) => ({ ...ctx, rrsp: showRrsp2(ctx.totals) }), (ctx) => ({ ...ctx, header: renderRow2(headerRow2(ctx.rrsp)) }), (ctx) => ({ ...ctx, bodyRows: ctx.rows.map((r2) => renderRow2(toRow2(r2, ctx.rrsp))) }), (ctx) => ({ ...ctx, totalsStr: renderRow2(totalsRow2(ctx.totals, ctx.rrsp)), W: ctx.header.length }), (ctx) => {
79987
+ var renderMonthlyTable = (monthly, year = 2026, province = "Ontario", annualSalary = 0, rrspPercent = 0) => t3(monthly, (ctx) => ({ ...ctx, rrsp: showRrsp2(ctx.totals) }), (ctx) => ({ ...ctx, header: renderRow2(headerRow2(ctx.rrsp)) }), (ctx) => ({ ...ctx, bodyRows: ctx.rows.map((r2) => renderRow2(toRow2(r2, ctx.rrsp))) }), (ctx) => ({ ...ctx, totalsStr: renderRow2(totalsRow2(ctx.totals, ctx.rrsp)), W: ctx.header.length }), (ctx) => {
79935
79988
  const t8 = ctx.totals;
79936
79989
  const empTotal = totalEmployeeRrspTotals2(t8);
79937
79990
  const rrspSummary = !ctx.rrsp ? "" : `
@@ -79939,6 +79992,9 @@ var renderMonthlyTable = (monthly, year = 2026) => t3(monthly, (ctx) => ({ ...ct
79939
79992
  RRSP Er: $${money(t8.rrspEmployer)}/yr
79940
79993
  RRSP Total (You + Er): $${money(empTotal + t8.rrspEmployer)}/yr`;
79941
79994
  const taxRate = t8.grossIncome > 0 ? t8.totalDeductions / t8.grossIncome * 100 : 0;
79995
+ const taxableIncome = annualSalary * (1 - rrspPercent / 100);
79996
+ const mr = marginalRate(year, province, taxableIncome);
79997
+ const marginalLine = mr.isOk() ? ` Marginal tax rate: ${pct(mr.value.combined * 100)} (fed ${pct(mr.value.federal * 100)} + prov ${pct(mr.value.provincial * 100)})` : "";
79942
79998
  return `Monthly Table (${year})
79943
79999
  ${line("═", ctx.W)}
79944
80000
  ${ctx.header}
@@ -79948,7 +80004,8 @@ ${ctx.bodyRows.join(`
79948
80004
  ${line("─", ctx.W)}
79949
80005
  ${ctx.totalsStr}
79950
80006
  ${line("═", ctx.W)}
79951
- Effective tax rate: ${pct(taxRate)}${rrspSummary}`;
80007
+ Effective tax rate: ${pct(taxRate)}${marginalLine ? `
80008
+ ${marginalLine}` : ""}${rrspSummary}`;
79952
80009
  });
79953
80010
 
79954
80011
  // src/monthly.ts
@@ -80306,11 +80363,12 @@ var runYearlyMode = async (config2, headless, svc, flags) => {
80306
80363
  }
80307
80364
  const yearly = yearlyResult.value;
80308
80365
  const periodsPerYear = PAY_PERIODS[config2.payPeriod];
80366
+ const rrspPercent = config2.rrspMatchPercent + config2.rrspUnmatchedPercent;
80309
80367
  if (flags.table)
80310
- console.log(renderTable(yearly, periodsPerYear, config2.year));
80368
+ console.log(renderTable(yearly, periodsPerYear, config2.year, config2.province, config2.annualSalary, rrspPercent));
80311
80369
  if (flags.monthTable) {
80312
80370
  const monthly = groupByMonth(yearly, config2.year, config2.payPeriod, periodsPerYear);
80313
- console.log(renderMonthlyTable(monthly, config2.year));
80371
+ console.log(renderMonthlyTable(monthly, config2.year, config2.province, config2.annualSalary, rrspPercent));
80314
80372
  }
80315
80373
  if (flags.annual)
80316
80374
  console.log(renderAnnual(yearly.totals));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seethruhead/cra-payroll",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Calculate Canadian payroll deductions using CRA's Payroll Deductions Online Calculator",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "build": "bun build --compile src/cli.ts --outfile cra-payroll --external electron",
15
15
  "build:npm": "bun build src/cli.ts --outfile dist/cra-payroll.js --target=node",
16
16
  "release": "bash release.sh",
17
- "test": "bun test src/unit.test.ts src/cache.test.ts src/monthly.test.ts src/views.test.ts",
17
+ "test": "bun test src/unit.test.ts src/cache.test.ts src/monthly.test.ts src/views.test.ts src/brackets.test.ts",
18
18
  "test:integration": "bun test src/integration.test.ts --timeout 120000 --max-concurrency 1",
19
19
  "test:all": "bun test --timeout 120000 --max-concurrency 1"
20
20
  },