From becdf53f1a8063780008df808f5b31189d1c0170 Mon Sep 17 00:00:00 2001 From: krisf Date: Fri, 9 Oct 2026 11:54:12 -0400 Subject: [PATCH] PS pension: add case K (G1 55+ min(F1,F2) lowest-reduction rule, PSPC-verified) + report wording fixes --- PS-PENSION-REPORT.md | 14 ++++++++------ tests/ps-pension.test.js | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/PS-PENSION-REPORT.md b/PS-PENSION-REPORT.md index 52e84c4..ea832b1 100644 --- a/PS-PENSION-REPORT.md +++ b/PS-PENSION-REPORT.md @@ -58,8 +58,9 @@ Eligibility & early reduction (annual allowance), ages/service to 1/10 year, reduction applies to the **whole** pension, permanent: - **G1 (joined ≤ 2012-12-31):** unreduced at 60+ w/ ≥2 yrs, or 55–59 w/ ≥30. - S<25: F1 = 5%×(60−age). S≥25: F2 = max(5%×(55−age), 5%×(30−S)); at 60+ w/ S≥25, - min(F1,F2) applies; leaving <50 → F1 pinned at 50 → 50% cap (`note: f1Under50`). + S<25: F1 = 5%×(60−age). S≥25: F2 = max(5%×(55−age), 5%×(30−S)); at 55+ w/ S≥25, + min(F1,F2) applies (PSPC: "compare Formula 1 and 2 and apply the lowest reduction"); + leaving <50 → F1 pinned at 50 → 50% cap (`note: f1Under50`). - **G2 (joined ≥ 2013-01-01):** unreduced at 65+ w/ ≥2, or 60–64 w/ ≥30. S<25: F1 = 5%×(65−age). S≥25: F2 = max(5%×(60−age), 5%×(30−S)); at 60+ w/ S≥25, min(F1,F2). Leaving <55: modelled on the **PSPC-default path — deferred annuity, @@ -67,7 +68,7 @@ reduction applies to the **whole** pension, permanent: the 50%-at-55 annual-allowance alternative is surfaced as a note). - <2 yrs service → return of contributions only (pension 0, note `lt2`). -IT salary picker (occupational group **IT**): 5 levels × 8 steps, real 2024-12-22 +IT salary picker (occupational group **IT**): 5 levels × 8 steps (IT-05 has 9), real 2024-12-22 rates (IT-01 $69,361–89,382 … IT-05 $133,582–174,076), fully overridable — the step selection pre-fills the salary field, which stays editable. @@ -117,12 +118,13 @@ insight, disclaimer. FR uses official terms (RRFP/PSCC, **prestation de raccorde | Suite | Result | |---|---| -| `node tests/ps-pension.test.js` | **63 pass, 0 fail** | +| `node tests/ps-pension.test.js` | **70 pass, 0 fail** (63 + case K added post-deploy by Hermes: G1 55+ min(F1,F2)) | | `node tests/engine.test.js` (regression) | **36 pass, 0 fail** | | `node tests/i18n.test.js` | **I18N-OK (48)** | | `node tests/ui.smoke.js` (EN+FR quiz→results, + PS module flows) | **SMOKE-OK (74)** | -Plan cases **A–J verified to the cent** (A/B/C/D/E/F/G/H/I/J per plan §tests), plus +Plan cases **A–K verified to the cent** (A–J per plan §tests; K added post-deploy by Hermes — +the G1 55+ "lowest of F1/F2" branch was untested), plus engine integration: bridge present at 64 / absent at 65; deferred pension gated (startAge 65 ⇒ no income/tax/clawback before 65); module-off legacy behaviour; OAS clawback gating at 65+; pre-65 couple splitting lowers joint tax @@ -176,7 +178,7 @@ OAS clawback gating at 65+; pre-65 couple splitting lowers joint tax - [x] Results card with full breakdown + dated sources; insight; chart series - [x] Pension flows through income/tax/PIA/GIS/splitting incl. pre-65 life-annuity splitting - [x] Full EN + FR (parity 48/48, FR smoke green) -- [x] Tests: ps-pension 63/63 · engine 36/36 · i18n OK · smoke 74/74 +- [x] Tests: ps-pension 70/70 · engine 36/36 · i18n OK · smoke 74/74 - [x] Audits EN+FR @320/360/390 + desktop + off-LAN (evidence in `audit/`) - [x] Deployed (DEPLOY-OK) and reachable off-LAN (OFFLAN-OK) - [x] This report + evidence committed & pushed diff --git a/tests/ps-pension.test.js b/tests/ps-pension.test.js index 97440b2..30878af 100644 --- a/tests/ps-pension.test.js +++ b/tests/ps-pension.test.js @@ -1,5 +1,7 @@ // Retire-planner PS pension tests — run: node tests/ps-pension.test.js (PS-PENSION-PLAN.md Stage 3) -// Hand-computed cases A–J from the plan (AMPE proxy = 2026 YMPE = 74,600), verified to the cent, +// Hand-computed cases A–K from the plan (AMPE proxy = 2026 YMPE = 74,600), verified to the cent, +// plus K added post-deploy (Hermes, 2026-10-09): G1 55+ with S≥25 must apply PSPC's +// "compare Formula 1 and 2 and apply the lowest reduction" — the subtlest branch, was untested. // plus engine integration checks (bridge window, start-age gating, PIA gating, pre-65 splitting). // // NOTE (documented in PS-PENSION-REPORT.md): two plan cases contain arithmetic typos. @@ -136,6 +138,19 @@ console.log("---- J. service 1.5 yrs: return of contributions only ----"); ok("J notes lt2", r.notes.indexOf("lt2") >= 0, JSON.stringify(r.notes)); } +console.log("---- K. G1, 70k, 25 yrs, retire 59: PSPC 55+ rule — lowest of F1 5% / F2 25% = 5% ----"); +{ + const r = calc(G1, 59, 25, 70000); + ok("K pct 5% min(F1,F2) at 55+ (PSPC: compare F1 and 2, apply the lowest)", cent(r.reductionPct, 0.05) && r.formula === "min(F1,F2)", r.reductionPct + " " + r.formula); + ok("K lifetime 24,062.50", cent(r.lifetime, 24062.50), r.lifetime); + ok("K bridge 10,937.50", cent(r.bridge, 10937.50), r.bridge); + ok("K atRetirement 33,250.00 (35,000 × 0.95)", cent(r.pensionAtRetirement, 33250.00), r.pensionAtRetirement); + ok("K from65 22,859.38 (24,062.50 × 0.95)", cent(r.pensionFrom65, 22859.38), r.pensionFrom65); + const r2 = calc(G1, 56, 25, 70000); + ok("K2 pct 20% min(F1 20% / F2 25%) at 56", cent(r2.reductionPct, 0.20) && r2.formula === "min(F1,F2)", r2.reductionPct + " " + r2.formula); + ok("K2 atRetirement 28,000.00 (35,000 × 0.80)", cent(r2.pensionAtRetirement, 28000.00), r2.pensionAtRetirement); +} + console.log("---- Engine integration ----"); const base = Object.assign({}, E.defaults, { age: 55, retireAge: 60, household: "single", province: "ON",