Retire Planner v1: engine (live 2026 CPP/OAS/GIS/RRIF/tax), 17-step UI, 4 SVG charts, vision gallery, tests
This commit is contained in:
commit
e25dd4e0a1
29 files changed
+3143
No files matched your search
@@ -0,0 +1,122 @@
|
||||
// Build site/engine/canada.js from fetched 2025/2026 sources (PLAN.md Stage 1).
|
||||
// Sources: CRA current-year (2026) tax-rates page (fetched 2026 via harness), ESDC quarterly
|
||||
// report Apr-Jun 2026, OAS payments page (Oct-Dec 2026 block), OAS recovery-tax page,
|
||||
// taxtips.ca province pages (brackets 2026+2025, BPA, surtax FAQ, QC brackets).
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const FETCH = path.join(__dirname, "..", ".fetch");
|
||||
const OUT = path.join(__dirname, "..", "site", "engine", "canada.js");
|
||||
|
||||
const ASOF_CRA = "2026-06-25"; // CRA current-year (2026) page publish
|
||||
const ASOF_ESDC = "2026-04-01"; // quarterly report Apr-Jun 2026 (CPP 2026 figures)
|
||||
const ASOF_OAS = "2026-09-29"; // payments page Oct-Dec 2026 block
|
||||
const ASOF_TT = "2026-02-22"; // taxtips dateModified (fetched today)
|
||||
|
||||
function num(s) { return Number(String(s).replace(/[$,\s]/g, "")); }
|
||||
|
||||
// ---------- province tables (taxtips) ----------
|
||||
function provinceTable(p) {
|
||||
const html = fs.readFileSync(path.join(FETCH, "tt_" + p + ".htm"), "utf8");
|
||||
const first = html.match(/<table[\s\S]*?<\/table>/i); // province table is first on page
|
||||
if (!first) return [];
|
||||
const rows = [...first[0].matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)];
|
||||
const out = [];
|
||||
for (const tr of rows) {
|
||||
const cells = [...tr[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi)]
|
||||
.map(c => c[1].replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim());
|
||||
if (cells.length < 2) continue;
|
||||
// data row: [income26, rate26, income25, rate25, ...]
|
||||
const income26 = cells[0], rate26 = cells[1];
|
||||
let m;
|
||||
if ((m = income26.match(/^first \$([\d,]+)$/i))) out.push({ up: num(m[1]), rate: rate26 });
|
||||
else if ((m = income26.match(/^over \$[\d,]+ up to \$([\d,]+)$/i))) out.push({ up: num(m[1]), rate: rate26 });
|
||||
else if ((m = income26.match(/^over \$([\d,]+)$/i))) out.push({ up: null, rate: rate26 });
|
||||
}
|
||||
return out.map(r => ({ up: r.up, rate: parseFloat(r.rate) / 100 }));
|
||||
}
|
||||
function faqBPA(p) {
|
||||
const t = fs.readFileSync(path.join(FETCH, "tt_" + p + ".htm"), "utf8")
|
||||
.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&#\d+;/g, " ")
|
||||
.replace(/&/g, "&").replace(/\s+/g, " ");
|
||||
const m = t.match(/basic personal amount for 2026 is \$([\d,]+)/i) ||
|
||||
t.match(/Basic Personal Amount 2026[^$]{0,200}\$([\d,]+)/i);
|
||||
return m ? num(m[1]) : null;
|
||||
}
|
||||
|
||||
const fed2026 = [
|
||||
{ up: 58523, rate: 0.14 }, { up: 117045, rate: 0.205 }, { up: 181440, rate: 0.26 },
|
||||
{ up: 258482, rate: 0.29 }, { up: null, rate: 0.33 }
|
||||
];
|
||||
|
||||
const provIds = {
|
||||
on: "Ontario", bc: "British Columbia", ab: "Alberta", mb: "Manitoba", sk: "Saskatchewan",
|
||||
ns: "Nova Scotia", nb: "New Brunswick", nl: "Newfoundland & Labrador", pe: "Prince Edward Island",
|
||||
yt: "Yukon", nt: "Northwest Territories", nu: "Nunavut", qc: "Quebec"
|
||||
};
|
||||
const prov = {};
|
||||
for (const [id, name] of Object.entries(provIds)) {
|
||||
const brackets = provinceTable(id);
|
||||
if (!brackets.length) throw new Error("no brackets parsed for " + id);
|
||||
const e = {
|
||||
name,
|
||||
brackets,
|
||||
bpa: faqBPA(id),
|
||||
source: "taxtips.ca/taxrates/" + id + ".htm (2026 table)",
|
||||
asOf: ASOF_TT
|
||||
};
|
||||
if (id === "on") e.surtax = [{ from: 6474, rate: 0.20 }, { from: 8102, rate: 0.36 }];
|
||||
if (id === "yt") e.bpa = 16452; // YT BPA tracks federal 2026 max (min/max table on page)
|
||||
prov[id.toUpperCase()] = e;
|
||||
}
|
||||
|
||||
// ---------- canada.ca fetched values (harness web_fetch, Oct 2026) ----------
|
||||
const E = "https://www.canada.ca/en/employment-social-development/programs/pensions/pension/statistics/2026-quarterly-april-june.html";
|
||||
const O = "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html";
|
||||
const R = "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/recovery-tax.html";
|
||||
const CRAU = "https://www.canada.ca/en/revenue-agency/services/tax/individuals/tax-rates-brackets/current-year.html";
|
||||
|
||||
// CRA statutory RRIF factor table (Reg. 8301(1), unchanged 2016–2026): 71→5.28 … 94→12.18, 95+→20.
|
||||
const rrifMin = {};
|
||||
{ const f = {71:5.28,72:5.40,73:5.53,74:5.67,75:5.82,76:5.98,77:6.15,78:6.33,79:6.53,80:6.74,
|
||||
81:6.97,82:7.22,83:7.48,84:7.77,85:8.08,86:8.41,87:8.77,88:9.15,89:9.56,90:10.00,
|
||||
91:10.48,92:11.00,93:11.56,94:12.18};
|
||||
for (const [a,v] of Object.entries(f)) rrifMin[a] = v;
|
||||
rrifMin[95] = 20.0; }
|
||||
|
||||
const doc = {
|
||||
generated: "2026-10-08",
|
||||
year: "2026 (live-fetched; plan fallbacks were 2025)",
|
||||
asOf: "October 2026 (CPP 2026, OAS/GIS Oct-Dec 2026, tax 2026)",
|
||||
cppMax65Monthly: { value: 1507.65, source: E + " Table 1, Jan 2026 (incl. CPP enhancement)", asOf: ASOF_ESDC },
|
||||
ympe: { value: 74600, source: E + " Table 2 YMPE 2026 (YBE $3,500)", asOf: ASOF_ESDC },
|
||||
oasMax6574Monthly: { value: 762.50, source: O + " Oct-Dec 2026", asOf: ASOF_OAS },
|
||||
oas75Boost: { value: 1.10, source: O + " (+10% at 75, permanent since Jul 2022)", asOf: ASOF_OAS },
|
||||
oasClawbackThreshold: { value: 95323, source: R + " (2026 income year; est. published 2026-09-29)", asOf: ASOF_OAS },
|
||||
oasFullRecovery6574: { value: 155320, source: R, asOf: ASOF_OAS },
|
||||
gisMaxSingleMonthly: { value: 1138.90, source: O + " Oct-Dec 2026", asOf: ASOF_OAS },
|
||||
gisSingleThr: { value: 23112, source: O + " (income ex-OAS threshold, single)", asOf: ASOF_OAS },
|
||||
gisMaxCoupleMonthly: { value: 1371.12, source: O + " (685.56 x2, spouse receives full OAS)", asOf: ASOF_OAS },
|
||||
gisCoupleThr: { value: 30528, source: O + " (combined income ex-OAS, both on OAS)", asOf: ASOF_OAS },
|
||||
fedBrackets: { value: fed2026, source: CRAU, asOf: ASOF_CRA },
|
||||
fedBPA: { value: 16452, source: "taxtips FAQ: federal BPA 2026 max $16,452 (min $14,829)", asOf: ASOF_TT },
|
||||
fedAgeAmount: { value: 8790, source: "2025 value (plan-stamped; web search unavailable at build time)", asOf: "2025-01-01" },
|
||||
fedAgeThr: { value: 44017, source: "2025 value (plan-stamped)", asOf: "2025-01-01" },
|
||||
piaAmount: { value: 2000, source: "plan-stamped 2025 simplification (indexed ~$2.2k in 2026)", asOf: "2025-01-01" },
|
||||
cppEarlyAdjMonthly: { value: 0.006, source: "plan-stamped / canada.ca CPP (0.6% per month before 65)", asOf: "2025-01-01" },
|
||||
cppLateAdjMonthly: { value: 0.007, source: "plan-stamped (0.7% per month after 65)", asOf: "2025-01-01" },
|
||||
oasDeferralMonthly: { value: 0.006, source: E + " Table 5 footnote 8 (+0.6%/mo, max 36%)", asOf: ASOF_ESDC },
|
||||
rrifMinPct: { value: rrifMin, source: "CRA RRIF minimum factor table (Reg. 8301(1)) — CRA RRIF page 404 at fetch time, table stamped (plan fallback: 71→5.28 ✓)", asOf: "2026" },
|
||||
provBrackets: prov
|
||||
};
|
||||
|
||||
const body = "/* AUTO-GENERATED by scripts/build_canada.js from live sources — see PLAN.md Stage 1.\n" +
|
||||
" * Evidence JSON kept at scripts/constants-evidence.json */\n" +
|
||||
"(function (root) { root.RetireCanada = " + JSON.stringify(doc, null, 2) + ";\n" +
|
||||
" if (typeof module !== 'undefined' && module.exports) module.exports = root.RetireCanada;\n" +
|
||||
"})(typeof self !== 'undefined' ? self : this);\n";
|
||||
fs.writeFileSync(OUT, body);
|
||||
fs.writeFileSync(path.join(__dirname, "constants-evidence.json"), JSON.stringify(doc, null, 2));
|
||||
|
||||
// sanity
|
||||
console.log("fed brackets:", JSON.stringify(fed2026));
|
||||
for (const [k, v] of Object.entries(prov)) console.log(k, "bpa", v.bpa, "n", v.brackets.length, "first", JSON.stringify(v.brackets[0]), "top", JSON.stringify(v.brackets[v.brackets.length - 1]));
|
||||
Reference in new issue
Block a user