ps: auto-derive years of service from join year + retirement age

This commit is contained in:
krisf committed 2026-10-09 15:41:58 -04:00
1 parent a0fbb390b5
commit 97cecb91a7
17 files changed
+667 -18

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
// PS-AUTO-SERVICE tests — auto-derivation of "years of pensionable service"
// (join year + retirement age + current age). Run: node tests/ps-auto-service.test.js
// Derivation (canonical, PS-AUTO-SERVICE plan):
// worked = currentYear − joinYear ; joinAge = age − worked ; s = retireAge − joinAge ; clamp [0, 60]
'use strict';
const P = require("../site/engine/pspension.js");
let pass = 0, fail = 0;
function ok(name, cond, detail) {
if (cond) { pass++; console.log(" PASS " + name + (detail !== undefined ? " [" + detail + "]" : "")); }
else { fail++; console.log(" FAIL " + name + (detail !== undefined ? " [" + detail + "]" : "")); }
}
const A = P.autoServiceYears;
console.log("PS auto-service tests (PS.autoServiceYears)");
console.log("---- plan cases ----");
ok("age 42, joined 2000, retire 65, year 2026 → joined at 16 → 49", A(42, 2000, 65, 2026) === 49, A(42, 2000, 65, 2026));
ok("age 30, joined 2020, retire 65, year 2026 → joined at 24 → 41", A(30, 2020, 65, 2026) === 41, A(30, 2020, 65, 2026));
ok("join year in the future (2030 @ age 40) → clamp to 0", A(40, 2030, 40, 2026) === 0, A(40, 2030, 40, 2026));
ok("retireAge < ageAtJoin (age 60, join 2020 → joined at 54, retire 50) → 0", A(60, 2020, 50, 2026) === 0, A(60, 2020, 50, 2026));
ok("retireAge = ageAtJoin (joined at 54, retire 54) → 0", A(60, 2020, 54, 2026) === 0, A(60, 2020, 54, 2026));
console.log("---- missing / invalid → null ----");
ok("age missing → null", A(null, 2000, 65, 2026) === null);
ok("age '' → null", A("", 2000, 65, 2026) === null);
ok("age 'abc' → null", A("abc", 2000, 65, 2026) === null);
ok("joinYear missing → null", A(42, null, 65, 2026) === null);
ok("joinYear '' → null", A(42, "", 65, 2026) === null);
ok("joinYear 999 (not a year) → null", A(42, 999, 65, 2026) === null);
ok("retireAge missing → null", A(42, 2000, null, 2026) === null);
ok("retireAge 0 → null", A(42, 2000, 0, 2026) === null);
ok("age 0 → null", A(0, 2000, 65, 2026) === null);
console.log("---- clamps + shapes ----");
ok("upper clamp 60 (age 20, joined 1960, retire 65 → raw 111 → 60)", A(20, 1960, 65, 2026) === 60, A(20, 1960, 65, 2026));
ok("whole years (age 43, join 2001, retire 65 → worked 25, joined at 18 → 47)", A(43, 2001, 65, 2026) === 47, A(43, 2001, 65, 2026));
ok("string inputs accepted ('42','2000','65','2026' → 49)", A("42", "2000", "65", "2026") === 49, A("42", "2000", "65", "2026"));
ok("currentYear defaults to the calendar year (joined this year, retire 65, age 25 → 40±1)",
Math.abs(A(25, new Date().getFullYear(), 65) - 40) <= 1, A(25, new Date().getFullYear(), 65));
console.log("---- engine contract (35-yr cap stays the engine's job) ----");
{
const s = A(42, 2000, 65, 2026); // 49
const r = P.computePSPension({ joinYear: 2000, retireAge: 65, serviceYears: s, avgSalary: 100000, ampe: 74600 });
ok("auto 49 feeds the engine → serviceCapped 35", r.serviceCapped === 35, r.serviceCapped);
ok("auto 49 unreduced at 65 (G1, S≥2): atRetirement = 0.01375·74600·35 + 0.02·25400·35 = 53,681.25",
Math.abs(r.pensionAtRetirement - (0.01375 * 74600 * 35 + 0.02 * 25400 * 35)) < 0.005, r.pensionAtRetirement);
const s2 = A(30, 2020, 65, 2026); // 41 → capped 35
const r2 = P.computePSPension({ joinYear: 2020, retireAge: 65, serviceYears: s2, avgSalary: 100000, ampe: 74600 });
ok("auto 41 → serviceCapped 35 (G2)", r2.serviceCapped === 35 && r2.group === 2, r2.serviceCapped + " g" + r2.group);
}
console.log("\nRESULT: " + pass + " pass, " + fail + " fail");
process.exit(fail ? 1 : 0);