@seethruhead/cra-payroll 0.3.1 → 0.5.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.
Files changed (2) hide show
  1. package/dist/cra-payroll.js +242 -25
  2. package/package.json +2 -2
@@ -69212,8 +69212,8 @@ var init_LaunchOptions = __esm(() => {
69212
69212
 
69213
69213
  // src/cli.ts
69214
69214
  import { parseArgs } from "util";
69215
- import { resolve as resolve6 } from "path";
69216
- import { existsSync as existsSync4, readFileSync as readFileSync4, fstatSync } from "fs";
69215
+ import { resolve as resolve7 } from "path";
69216
+ import { existsSync as existsSync5, readFileSync as readFileSync5, fstatSync } from "fs";
69217
69217
  import { createInterface as createInterface2 } from "readline";
69218
69218
 
69219
69219
  // node_modules/neverthrow/dist/index.cjs.js
@@ -79486,10 +79486,73 @@ ${text.value}
79486
79486
  await session.close();
79487
79487
  return parsed;
79488
79488
  };
79489
+ // src/cache.ts
79490
+ import { resolve as resolve6 } from "path";
79491
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
79492
+ import { createHash } from "crypto";
79493
+ var DEFAULT_CACHE_DIR = resolve6(process.env.HOME || "~", ".config", "cra-payroll", "cache");
79494
+ var ensureDir = (dir) => {
79495
+ if (!existsSync3(dir))
79496
+ mkdirSync(dir, { recursive: true });
79497
+ };
79498
+ var cacheKey = (config2) => {
79499
+ const data = JSON.stringify({
79500
+ province: config2.province,
79501
+ annualSalary: config2.annualSalary,
79502
+ payPeriod: config2.payPeriod,
79503
+ year: config2.year,
79504
+ rrspMatchPercent: config2.rrspMatchPercent,
79505
+ rrspUnmatchedPercent: config2.rrspUnmatchedPercent,
79506
+ cppMaxedOut: config2.cppMaxedOut,
79507
+ eiMaxedOut: config2.eiMaxedOut
79508
+ });
79509
+ return createHash("sha256").update(data).digest("hex").slice(0, 16);
79510
+ };
79511
+ var cachePath = (config2, dir) => resolve6(dir, `${cacheKey(config2)}.json`);
79512
+ var readCache = (config2, dir) => {
79513
+ const path12 = cachePath(config2, dir);
79514
+ if (!existsSync3(path12))
79515
+ return null;
79516
+ try {
79517
+ const data = JSON.parse(readFileSync4(path12, "utf-8"));
79518
+ if (typeof data.grossIncome === "number" && typeof data.net === "number") {
79519
+ return data;
79520
+ }
79521
+ return null;
79522
+ } catch {
79523
+ return null;
79524
+ }
79525
+ };
79526
+ var writeCache = (config2, result, dir) => {
79527
+ try {
79528
+ ensureDir(dir);
79529
+ writeFileSync(cachePath(config2, dir), JSON.stringify(result, null, 2));
79530
+ } catch (e2) {
79531
+ log(`cache write failed: ${e2.message}`);
79532
+ }
79533
+ };
79534
+ var withCache = (inner, cacheDir = DEFAULT_CACHE_DIR) => ({
79535
+ calculate: async (config2, headless) => {
79536
+ const cached = readCache(config2, cacheDir);
79537
+ if (cached) {
79538
+ log(`cache hit: ${cachePath(config2, cacheDir)}`);
79539
+ return $ok(cached);
79540
+ }
79541
+ log(`cache miss, hitting CRA...`);
79542
+ const result = await inner.calculate(config2, headless);
79543
+ if (result.isOk()) {
79544
+ writeCache(config2, result.value, cacheDir);
79545
+ }
79546
+ return result;
79547
+ }
79548
+ });
79549
+
79489
79550
  // src/calculator.ts
79490
- var craService = {
79551
+ var rawService = {
79491
79552
  calculate: calculatePayroll
79492
79553
  };
79554
+ var craService = withCache(rawService);
79555
+ var craServiceNoCache = rawService;
79493
79556
 
79494
79557
  // src/yearly.ts
79495
79558
  var CPP_MAX_BASE = 4230.45;
@@ -79563,11 +79626,11 @@ var calculateYearly = async (service, config2, headless = false) => {
79563
79626
 
79564
79627
  // src/updater.ts
79565
79628
  import { execSync as execSync2 } from "child_process";
79566
- import { existsSync as existsSync3, renameSync, unlinkSync, chmodSync } from "fs";
79629
+ import { existsSync as existsSync4, renameSync, unlinkSync, chmodSync } from "fs";
79567
79630
  // package.json
79568
79631
  var package_default = {
79569
79632
  name: "@seethruhead/cra-payroll",
79570
- version: "0.3.1",
79633
+ version: "0.5.0",
79571
79634
  description: "Calculate Canadian payroll deductions using CRA's Payroll Deductions Online Calculator",
79572
79635
  type: "module",
79573
79636
  bin: {
@@ -79581,7 +79644,7 @@ var package_default = {
79581
79644
  build: "bun build --compile src/cli.ts --outfile cra-payroll --external electron",
79582
79645
  "build:npm": "bun build src/cli.ts --outfile dist/cra-payroll.js --target=node",
79583
79646
  release: "bash release.sh",
79584
- test: "bun test src/unit.test.ts",
79647
+ test: "bun test src/unit.test.ts src/cache.test.ts src/monthly.test.ts",
79585
79648
  "test:integration": "bun test src/integration.test.ts --timeout 120000 --max-concurrency 1",
79586
79649
  "test:all": "bun test --timeout 120000 --max-concurrency 1"
79587
79650
  },
@@ -79706,13 +79769,13 @@ var selfUpdate = async () => {
79706
79769
  console.log(`Downloading ${update.downloadUrl}...
79707
79770
  `);
79708
79771
  let binaryPath = "";
79709
- if (process.execPath && existsSync3(process.execPath) && !process.execPath.endsWith("/bun")) {
79772
+ if (process.execPath && existsSync4(process.execPath) && !process.execPath.endsWith("/bun")) {
79710
79773
  binaryPath = process.execPath;
79711
79774
  }
79712
79775
  if (!binaryPath) {
79713
79776
  try {
79714
79777
  const which = execSync2("which cra-payroll", { encoding: "utf-8" }).trim();
79715
- if (which && existsSync3(which))
79778
+ if (which && existsSync4(which))
79716
79779
  binaryPath = which;
79717
79780
  } catch {}
79718
79781
  }
@@ -79825,11 +79888,155 @@ ${ctx.totalsStr}
79825
79888
  ${line("═", ctx.W)}${rrspSummary}`;
79826
79889
  });
79827
79890
 
79891
+ // src/views/monthlyTable.ts
79892
+ var col2 = (v, w) => typeof v === "number" ? money(v).padStart(w) : v.padStart(w);
79893
+ var renderRow2 = (r2) => `${r2.label} │ ${col2(r2.gross, 10)} │ ${col2(r2.fedTax, 10)} │ ${col2(r2.provTax, 10)} │ ${col2(r2.cpp, 10)} │ ${col2(r2.cpp2, 6)} │ ${col2(r2.ei, 10)} │ ${col2(r2.netPay, 10)}${r2.rrspYou !== undefined ? ` │ ${col2(r2.rrspYou, 10)} │ ${col2(r2.rrspEr, 10)} │ ${col2(r2.takeHome, 10)}` : ""}`;
79894
+ var totalEmployeeRrsp2 = (r2) => r2.rrspMatched + r2.rrspUnmatched;
79895
+ var totalEmployeeRrspTotals2 = (t8) => t8.rrspMatched + t8.rrspUnmatched;
79896
+ var showRrsp2 = (totals) => totals.rrspMatched > 0 || totals.rrspUnmatched > 0 || totals.rrspEmployer > 0;
79897
+ var rrspFields2 = (show, fields) => show ? fields : {};
79898
+ var toRow2 = (r2, rrsp) => ({
79899
+ label: ` ${r2.month} `,
79900
+ gross: r2.grossIncome,
79901
+ fedTax: r2.federalTax,
79902
+ provTax: r2.provincialTax,
79903
+ cpp: r2.cpp,
79904
+ cpp2: r2.cpp2,
79905
+ ei: r2.ei,
79906
+ netPay: r2.netPay,
79907
+ ...rrspFields2(rrsp, { rrspYou: totalEmployeeRrsp2(r2), rrspEr: r2.rrspEmployer, takeHome: r2.netPay - totalEmployeeRrsp2(r2) })
79908
+ });
79909
+ var headerRow2 = (rrsp) => ({
79910
+ label: " ",
79911
+ gross: "Gross",
79912
+ fedTax: "Fed Tax",
79913
+ provTax: "Prov Tax",
79914
+ cpp: "CPP",
79915
+ cpp2: "CPP2",
79916
+ ei: "EI",
79917
+ netPay: "Net Pay",
79918
+ ...rrspFields2(rrsp, { rrspYou: "RRSP You", rrspEr: "RRSP Er", takeHome: "Take Home" })
79919
+ });
79920
+ var totalsRow2 = (totals, rrsp) => ({
79921
+ label: " TOT ",
79922
+ gross: totals.grossIncome,
79923
+ fedTax: totals.federalTax,
79924
+ provTax: totals.provincialTax,
79925
+ cpp: totals.cpp,
79926
+ cpp2: totals.cpp2,
79927
+ ei: totals.ei,
79928
+ netPay: totals.netPay,
79929
+ ...rrspFields2(rrsp, { rrspYou: totalEmployeeRrspTotals2(totals), rrspEr: totals.rrspEmployer, takeHome: totals.netPay - totalEmployeeRrspTotals2(totals) })
79930
+ });
79931
+ 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) => {
79932
+ const t8 = ctx.totals;
79933
+ const empTotal = totalEmployeeRrspTotals2(t8);
79934
+ const rrspSummary = !ctx.rrsp ? "" : `
79935
+ RRSP You: $${money(empTotal)}/yr${t8.rrspMatched > 0 ? ` — matched: $${money(t8.rrspMatched)}/yr` : ""}${t8.rrspUnmatched > 0 ? ` — unmatched: $${money(t8.rrspUnmatched)}/yr` : ""}
79936
+ RRSP Er: $${money(t8.rrspEmployer)}/yr
79937
+ RRSP Total (You + Er): $${money(empTotal + t8.rrspEmployer)}/yr`;
79938
+ return `Monthly Table (${year})
79939
+ ${line("═", ctx.W)}
79940
+ ${ctx.header}
79941
+ ${line("─", ctx.W)}
79942
+ ${ctx.bodyRows.join(`
79943
+ `)}
79944
+ ${line("─", ctx.W)}
79945
+ ${ctx.totalsStr}
79946
+ ${line("═", ctx.W)}${rrspSummary}`;
79947
+ });
79948
+
79949
+ // src/monthly.ts
79950
+ var MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
79951
+ var isWeekday = (d) => {
79952
+ const dow = d.getDay();
79953
+ return dow !== 0 && dow !== 6;
79954
+ };
79955
+ var nextWeekday = (d) => {
79956
+ const next = new Date(d);
79957
+ while (!isWeekday(next))
79958
+ next.setDate(next.getDate() + 1);
79959
+ return next;
79960
+ };
79961
+ var addDays = (d, n5) => {
79962
+ const next = new Date(d);
79963
+ next.setDate(next.getDate() + n5);
79964
+ return next;
79965
+ };
79966
+ var dailyPayMonths = (year, count) => {
79967
+ const months = new Array(12).fill(0);
79968
+ let d = nextWeekday(new Date(year, 0, 1));
79969
+ for (let i2 = 0;i2 < count; i2++) {
79970
+ months[d.getMonth()]++;
79971
+ d = nextWeekday(addDays(d, 1));
79972
+ }
79973
+ return months;
79974
+ };
79975
+ var weeklyPayMonths = (year, count, everyNWeeks) => {
79976
+ const months = new Array(12).fill(0);
79977
+ let d = new Date(year, 0, 1);
79978
+ while (d.getDay() !== 5)
79979
+ d.setDate(d.getDate() + 1);
79980
+ for (let i2 = 0;i2 < count; i2++) {
79981
+ months[d.getMonth()]++;
79982
+ d = addDays(d, 7 * everyNWeeks);
79983
+ }
79984
+ return months;
79985
+ };
79986
+ var semiMonthlyPayMonths = () => new Array(12).fill(2);
79987
+ var monthlyPayMonths = () => new Array(12).fill(1);
79988
+ var evenPayMonths = (count) => {
79989
+ const base = Math.floor(count / 12);
79990
+ const extra = count % 12;
79991
+ return t5(0, 12).map((i2) => base + (i2 < extra ? 1 : 0));
79992
+ };
79993
+ var periodsPerMonth = (year, payPeriod, totalPeriods) => {
79994
+ if (payPeriod.includes("Daily"))
79995
+ return dailyPayMonths(year, totalPeriods);
79996
+ if (payPeriod.includes("Semi-monthly"))
79997
+ return semiMonthlyPayMonths();
79998
+ if (payPeriod.startsWith("Monthly"))
79999
+ return monthlyPayMonths();
80000
+ if (payPeriod.includes("Biweekly"))
80001
+ return weeklyPayMonths(year, totalPeriods, 2);
80002
+ if (payPeriod.includes("Weekly"))
80003
+ return weeklyPayMonths(year, totalPeriods, 1);
80004
+ return evenPayMonths(totalPeriods);
80005
+ };
80006
+ var round22 = (n5) => Math.round(n5 * 100) / 100;
80007
+ var sumRows = (rows) => ({
80008
+ grossIncome: round22(t7(rows, (r2) => r2.grossIncome)),
80009
+ rrspMatched: round22(t7(rows, (r2) => r2.rrspMatched)),
80010
+ rrspUnmatched: round22(t7(rows, (r2) => r2.rrspUnmatched)),
80011
+ rrspEmployer: round22(t7(rows, (r2) => r2.rrspEmployer)),
80012
+ federalTax: round22(t7(rows, (r2) => r2.federalTax)),
80013
+ provincialTax: round22(t7(rows, (r2) => r2.provincialTax)),
80014
+ cpp: round22(t7(rows, (r2) => r2.cpp)),
80015
+ cpp2: round22(t7(rows, (r2) => r2.cpp2)),
80016
+ ei: round22(t7(rows, (r2) => r2.ei)),
80017
+ totalDeductions: round22(t7(rows, (r2) => r2.totalDeductions)),
80018
+ netPay: round22(t7(rows, (r2) => r2.netPay))
80019
+ });
80020
+ var groupByMonth = (yearly, year, payPeriod, totalPeriods) => {
80021
+ const distribution = periodsPerMonth(year, payPeriod, totalPeriods);
80022
+ let offset = 0;
80023
+ const rows = distribution.map((count, i2) => {
80024
+ const chunk = yearly.rows.slice(offset, offset + count);
80025
+ offset += count;
80026
+ return {
80027
+ month: MONTH_NAMES[i2],
80028
+ periods: count,
80029
+ ...sumRows(chunk)
80030
+ };
80031
+ });
80032
+ return { rows, totals: yearly.totals };
80033
+ };
80034
+
79828
80035
  // src/views/summary.ts
79829
80036
  var W2 = 42;
79830
80037
  var renderBreakdown = (label, totals, divisor = 1) => {
79831
80038
  const f2 = (n5) => money(n5 / divisor).padStart(10);
79832
- const totalEmployeeRrsp2 = totals.rrspMatched + totals.rrspUnmatched;
80039
+ const totalEmployeeRrsp3 = totals.rrspMatched + totals.rrspUnmatched;
79833
80040
  return `
79834
80041
  ${label}
79835
80042
  ${line("═", W2)}
@@ -79842,7 +80049,7 @@ ${line("─", W2)}
79842
80049
  ${line("─", W2)}
79843
80050
  Total Deductions: -$${f2(totals.totalDeductions)}
79844
80051
  ${line("═", W2)}
79845
- Net Pay: $${f2(totals.netPay)}${when(totalEmployeeRrsp2, ` (after RRSP): $${money((totals.netPay - totalEmployeeRrsp2) / divisor).padStart(10)}`)}`;
80052
+ Net Pay: $${f2(totals.netPay)}${when(totalEmployeeRrsp3, ` (after RRSP): $${money((totals.netPay - totalEmployeeRrsp3) / divisor).padStart(10)}`)}`;
79846
80053
  };
79847
80054
  var renderAnnual = (totals) => renderBreakdown("Annual Totals", totals);
79848
80055
  var renderMonthly = (totals) => renderBreakdown("Monthly Averages", totals, 12);
@@ -79883,8 +80090,10 @@ var { values } = parseArgs({
79883
80090
  "cpp-maxed": { type: "boolean", default: false },
79884
80091
  "ei-maxed": { type: "boolean", default: false },
79885
80092
  table: { type: "boolean", short: "t", default: false },
80093
+ "month-table": { type: "boolean", short: "M", default: false },
79886
80094
  annual: { type: "boolean", short: "a", default: false },
79887
80095
  monthly: { type: "boolean", short: "m", default: false },
80096
+ "no-cache": { type: "boolean", default: false },
79888
80097
  update: { type: "boolean", default: false },
79889
80098
  version: { type: "boolean", default: false },
79890
80099
  headless: { type: "boolean", default: false },
@@ -79913,8 +80122,10 @@ if (values.help) {
79913
80122
  --cpp-maxed CPP contributions maxed out for the year
79914
80123
  --ei-maxed EI premiums maxed out for the year
79915
80124
  -t, --table Show per-paycheck table for the year (tracks CPP/EI max)
80125
+ -M, --month-table Show monthly table for the year
79916
80126
  -a, --annual Show annualized totals
79917
80127
  -m, --monthly Show monthly averages
80128
+ --no-cache Skip cache and force a fresh CRA lookup
79918
80129
  --headless Run browser headless (may be blocked by CRA)
79919
80130
  --update Self-update to the latest release
79920
80131
  --version Show current version
@@ -80009,14 +80220,14 @@ var readStdinConfig = async () => {
80009
80220
  };
80010
80221
  var readFileConfig = (configFlag) => {
80011
80222
  const configPaths = [
80012
- configFlag ? resolve6(configFlag) : "",
80013
- resolve6("config.json"),
80014
- resolve6(process.env.HOME || "~", ".config", "cra-payroll.json"),
80015
- resolve6(process.env.HOME || "~", ".cra-payroll.json")
80223
+ configFlag ? resolve7(configFlag) : "",
80224
+ resolve7("config.json"),
80225
+ resolve7(process.env.HOME || "~", ".config", "cra-payroll.json"),
80226
+ resolve7(process.env.HOME || "~", ".cra-payroll.json")
80016
80227
  ].filter(Boolean);
80017
80228
  for (const p of configPaths) {
80018
- if (p && existsSync4(p)) {
80019
- return JSON.parse(readFileSync4(p, "utf-8"));
80229
+ if (p && existsSync5(p)) {
80230
+ return JSON.parse(readFileSync5(p, "utf-8"));
80020
80231
  }
80021
80232
  }
80022
80233
  return {};
@@ -80032,7 +80243,7 @@ var loadFileConfig = async (configFlag, isPiped) => {
80032
80243
  try {
80033
80244
  const config2 = readFileConfig(configFlag);
80034
80245
  if (configFlag && Object.keys(config2).length === 0) {
80035
- return $err(`Config file not found: ${resolve6(configFlag)}`);
80246
+ return $err(`Config file not found: ${resolve7(configFlag)}`);
80036
80247
  }
80037
80248
  return $ok(config2);
80038
80249
  } catch (e2) {
@@ -80082,8 +80293,8 @@ var resolveConfig = async (vals, fileConfig, isPiped) => {
80082
80293
  eiMaxedOut
80083
80294
  });
80084
80295
  };
80085
- var runYearlyMode = async (config2, headless, flags) => {
80086
- const yearlyResult = await calculateYearly(craService, config2, headless);
80296
+ var runYearlyMode = async (config2, headless, svc, flags) => {
80297
+ const yearlyResult = await calculateYearly(svc, config2, headless);
80087
80298
  if (yearlyResult.isErr()) {
80088
80299
  console.error(`Error: ${yearlyResult.error}`);
80089
80300
  process.exit(1);
@@ -80092,13 +80303,17 @@ var runYearlyMode = async (config2, headless, flags) => {
80092
80303
  const periodsPerYear = PAY_PERIODS[config2.payPeriod];
80093
80304
  if (flags.table)
80094
80305
  console.log(renderTable(yearly, periodsPerYear, config2.year));
80306
+ if (flags.monthTable) {
80307
+ const monthly = groupByMonth(yearly, config2.year, config2.payPeriod, periodsPerYear);
80308
+ console.log(renderMonthlyTable(monthly, config2.year));
80309
+ }
80095
80310
  if (flags.annual)
80096
80311
  console.log(renderAnnual(yearly.totals));
80097
80312
  if (flags.monthly)
80098
80313
  console.log(renderMonthly(yearly.totals));
80099
80314
  };
80100
- var runSingleMode = async (config2, headless) => {
80101
- const calcResult = await calculatePayroll(config2, headless);
80315
+ var runSingleMode = async (config2, headless, svc) => {
80316
+ const calcResult = await svc.calculate(config2, headless);
80102
80317
  if (calcResult.isErr()) {
80103
80318
  console.error(`Error: ${calcResult.error}`);
80104
80319
  process.exit(1);
@@ -80129,15 +80344,17 @@ if (configResult.isErr()) {
80129
80344
  }
80130
80345
  var config2 = configResult.value;
80131
80346
  var headless = values.headless ?? false;
80347
+ var service = values["no-cache"] ? craServiceNoCache : craService;
80132
80348
  if (values.verbose)
80133
80349
  setVerbose(true);
80134
80350
  var wantTable = values.table ?? false;
80351
+ var wantMonthTable = values["month-table"] ?? false;
80135
80352
  var wantAnnual = values.annual ?? false;
80136
80353
  var wantMonthly = values.monthly ?? false;
80137
- console.log(renderConfig(config2, !wantTable && !wantAnnual && !wantMonthly));
80138
- if (wantTable || wantAnnual || wantMonthly) {
80139
- await runYearlyMode(config2, headless, { table: wantTable, annual: wantAnnual, monthly: wantMonthly });
80354
+ console.log(renderConfig(config2, !wantTable && !wantMonthTable && !wantAnnual && !wantMonthly));
80355
+ if (wantTable || wantMonthTable || wantAnnual || wantMonthly) {
80356
+ await runYearlyMode(config2, headless, service, { table: wantTable, monthTable: wantMonthTable, annual: wantAnnual, monthly: wantMonthly });
80140
80357
  } else {
80141
- await runSingleMode(config2, headless);
80358
+ await runSingleMode(config2, headless, service);
80142
80359
  }
80143
80360
  await showUpdateNag();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seethruhead/cra-payroll",
3
- "version": "0.3.1",
3
+ "version": "0.5.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",
17
+ "test": "bun test src/unit.test.ts src/cache.test.ts src/monthly.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
  },