ps: auto-derive years of service from join year + retirement age
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"http_code": "200",
|
||||||
|
"has_marker": true,
|
||||||
|
"app_has_override_state": true,
|
||||||
|
"app_has_usecalc_button": true,
|
||||||
|
"i18n_has_fr_auto_hint": true,
|
||||||
|
"engine_has_autoServiceYears": true,
|
||||||
|
"sizes": {
|
||||||
|
"app": 53443,
|
||||||
|
"i18n": 39173,
|
||||||
|
"ps": 12254
|
||||||
|
},
|
||||||
|
"checked_at": "2026-10-09T19:41:54Z",
|
||||||
|
"vantage": "trouble@100.96.20.61 (Midhurst, off-LAN)",
|
||||||
|
"all_ok": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"check": "EN auto: input prefilled 41 (age 45, joined 2005, retire 65) \u2014 untouched",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN auto: derivation hint",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Calculated: joined 2005 at age 24, retiring at 65 \u2192 41 years of service. Adjust only if you bought past service."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN auto: Next enabled + Use-calculated hidden",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "disabled=False uc_visible=False"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-auto-step: no horizontal overflow at 390px",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'iw': 390, 'docScrollW': 390, 'f1': {'top': 254, 'bottom': 306, 'left': 25, 'right': 365, 'w': 340}, 'next': {'top': 536, 'bottom': 586, 'left': 24, 'right': 390, 'w': 366}, 'uc':"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-auto-step: #f1 in viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 254, 'bottom': 306, 'left': 25, 'right': 365, 'w': 340}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-auto-step: #btn-next in viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 536, 'bottom': 586, 'left': 24, 'right': 390, 'w': 366}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN override (real typing): hint LIVE-swaps to override + auto",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Using your value (30 years). The calculation from your join year gives 41 years."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN override: \"Use calculated\" button becomes visible",
|
||||||
|
"pass": true,
|
||||||
|
"extra": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN override: persisted psServiceOverride = 30",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-override-step: no horizontal overflow at 390px",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'iw': 390, 'docScrollW': 390, 'f1': {'top': 254, 'bottom': 306, 'left': 25, 'right': 365, 'w': 340}, 'next': {'top': 536, 'bottom': 586, 'left': 24, 'right': 390, 'w': 366}, 'uc':"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-override-step: #f1 in viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 254, 'bottom': 306, 'left': 25, 'right': 365, 'w': 340}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-override-step: #btn-next in viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 536, 'bottom': 586, 'left': 24, 'right': 390, 'w': 366}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN use-calc: prefill back to 41 + auto hint",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "41 | Calculated: joined 2005 at age 24, retiring at 65 \u2192 41 years of service. Adjust only if yo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN use-calc: override cleared in localStorage",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "None"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN use-calc: button hidden again",
|
||||||
|
"pass": true,
|
||||||
|
"extra": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN results (override 30): 30 years used",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Your federal public service pension\nGroup 1 (rules from your join year)\t\nPensionable service used: 30 years (max 35)\t\n5-year average salary (highest paid)\t$108,"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN results (override 30): from-65 51,006.90",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Your federal public service pension\nGroup 1 (rules from your join year)\t\nPensionable service used: 30 years (max 35)\t\n5-year average salary (highest paid)\t$108,"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-results: no horizontal overflow at 390px",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'iw': 390, 'docScrollW': 390, 'f1': None, 'next': {'top': 0, 'bottom': 0, 'left': 0, 'right': 0, 'w': 0}, 'uc': None, 'card': {'top': 1933, 'bottom': 2732, 'left': 16, 'right': 37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-results: #btn-next in viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 0, 'bottom': 0, 'left': 0, 'right': 0, 'w': 0}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN-results: ps-card fits the viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 1933, 'bottom': 2732, 'left': 16, 'right': 374, 'w': 358}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN results (auto): 35 years used (auto 41, cap 35)",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Your federal public service pension\nGroup 1 (rules from your join year)\t\nPensionable service used: 35 years (max 35)\t\n5-year average salary (highest paid)\t$108,"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "EN results (auto): from-65 59,508.05",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Your federal public service pension\nGroup 1 (rules from your join year)\t\nPensionable service used: 35 years (max 35)\t\n5-year average salary (highest paid)\t$108,"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR auto: prefilled 41",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR auto: derivation hint",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Calcul\u00e9 : entr\u00e9e en poste en 2005 \u00e0 24 ans, d\u00e9part \u00e0 65 ans \u2192 41 ann\u00e9es de service. \u00c0 ajuster seulement si vous avez achet\u00e9 des services ant\u00e9rieurs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR override: hint",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Votre valeur (30 ann\u00e9es) est utilis\u00e9e. Le calcul \u00e0 partir de votre ann\u00e9e d\u2019entr\u00e9e donne 41 ans."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR override: \"Utiliser le calcul\" visible",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'display': 'block', 'vis': 'visible', 'box': [340, 20.921875], 'lang': 'fr'}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR results (auto): 35 ann\u00e9es (max 35)",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Votre pension de la fonction publique f\u00e9d\u00e9rale\nGroupe 1 (r\u00e8gles selon votre ann\u00e9e d\u2019adh\u00e9sion)\t\nService ouvrant droit \u00e0 pension retenu : 35 ann\u00e9es (max 35)\t\nSala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR results (auto): 59 508,05 formatted fr-CA",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Votre pension de la fonction publique f\u00e9d\u00e9rale\nGroupe 1 (r\u00e8gles selon votre ann\u00e9e d\u2019adh\u00e9sion)\t\nService ouvrant droit \u00e0 pension retenu : 35 ann\u00e9es (max 35)\t\nSala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR results (override): 30 ann\u00e9es + 51 006,90",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "Votre pension de la fonction publique f\u00e9d\u00e9rale\nGroupe 1 (r\u00e8gles selon votre ann\u00e9e d\u2019adh\u00e9sion)\t\nService ouvrant droit \u00e0 pension retenu : 30 ann\u00e9es (max 35)\t\nSalaire moyen sur 5 ans "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR-results: no horizontal overflow at 390px",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'iw': 390, 'docScrollW': 390, 'f1': None, 'next': {'top': 0, 'bottom': 0, 'left': 0, 'right': 0, 'w': 0}, 'uc': None, 'card': {'top': 1956, 'bottom': 2883, 'left': 16, 'right': 37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR-results: #btn-next in viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 0, 'bottom': 0, 'left': 0, 'right': 0, 'w': 0}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "FR-results: ps-card fits the viewport",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "{'top': 1956, 'bottom': 2883, 'left': 16, 'right': 374, 'w': 358}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "module OFF: 15 steps (last = 15/15)",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "15 / 15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "module OFF: no PS quiz text on results, card hidden",
|
||||||
|
"pass": true,
|
||||||
|
"extra": "[]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 237 KiB |
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Off-LAN check for the PS auto-service deploy (2026-10-09), via the Midhurst troublebox.
|
||||||
|
Verifies 200 + marker + served files carry the auto-service feature:
|
||||||
|
app.js → psServiceOverride ; i18n.js → FR auto hint string ; engine/pspension.js → autoServiceYears.
|
||||||
|
Writes audit/ps-auto-offlan-check.json, prints OFFLAN-OK/FAIL."""
|
||||||
|
import json, sys, time
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
CANDS = ["b1tvvt07", "ubnt", "admin", "password", "b1tvvt07!", "1234", "root", "glinet"]
|
||||||
|
URL = "https://retire.krisforbes.ca/"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conn = None
|
||||||
|
for pw in CANDS:
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
try:
|
||||||
|
c.connect("100.96.20.61", port=2222, username="trouble", password=pw,
|
||||||
|
timeout=25, banner_timeout=25, auth_timeout=25,
|
||||||
|
allow_agent=False, look_for_keys=False)
|
||||||
|
conn = c
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
c.close()
|
||||||
|
if conn is None:
|
||||||
|
print("OFFLAN-FAIL: no auth to troublebox")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
_, out, _ = conn.exec_command(cmd, timeout=60)
|
||||||
|
return out.read().decode("utf-8", "replace").strip()
|
||||||
|
|
||||||
|
code = run(f"curl -s -o /dev/null -w '%{{http_code}}' {URL} --max-time 30")
|
||||||
|
html = run(f"curl -s {URL} --max-time 30 | head -c 200000")
|
||||||
|
app = run(f"curl -s {URL}app.js --max-time 30 | head -c 500000")
|
||||||
|
i18n = run(f"curl -s {URL}i18n.js --max-time 30 | head -c 500000")
|
||||||
|
ps = run(f"curl -s {URL}engine/pspension.js --max-time 30 | head -c 500000")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
"http_code": code,
|
||||||
|
"has_marker": "retire-planner v1" in html,
|
||||||
|
"app_has_override_state": "psServiceOverride" in app,
|
||||||
|
"app_has_usecalc_button": "ps-usecalc" in app,
|
||||||
|
"i18n_has_fr_auto_hint": "entr" in i18n and "ann" in i18n and "de service. " in i18n and "Calcul" in i18n,
|
||||||
|
"engine_has_autoServiceYears": "autoServiceYears" in ps,
|
||||||
|
"sizes": {"app": len(app), "i18n": len(i18n), "ps": len(ps)},
|
||||||
|
"checked_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"vantage": "trouble@100.96.20.61 (Midhurst, off-LAN)",
|
||||||
|
}
|
||||||
|
checks["all_ok"] = (code == "200" and checks["has_marker"] and checks["app_has_override_state"]
|
||||||
|
and checks["app_has_usecalc_button"] and checks["i18n_has_fr_auto_hint"]
|
||||||
|
and checks["engine_has_autoServiceYears"])
|
||||||
|
with open("audit/ps-auto-offlan-check.json", "w") as f:
|
||||||
|
json.dump(checks, f, indent=2, ensure_ascii=False)
|
||||||
|
print(json.dumps(checks, indent=2, ensure_ascii=False))
|
||||||
|
print("OFFLAN-OK" if checks["all_ok"] else "OFFLAN-FAIL")
|
||||||
|
sys.exit(0 if checks["all_ok"] else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""PS-AUTO-SERVICE live UI audit — local server (site/) at 390x844, EN + FR.
|
||||||
|
Phases (each = seeded localStorage + reload, plus real keyboard interaction):
|
||||||
|
1 ps_service prefilled with the auto value + derivation hint, Next enabled, no button
|
||||||
|
2 real typing of an override (30) → override hint + "Use calculated" button
|
||||||
|
3 "Use calculated" restores the auto prefill + clears the override in localStorage
|
||||||
|
4 results card reflects the override (30 years · 51,006.90 / 51 506,90 FR)
|
||||||
|
5 results card reflects the auto value (35 years cap · 59,508.05 / 59 508,05 FR)
|
||||||
|
6 FR wording for 1/2/5
|
||||||
|
7 module OFF parity: 15 steps, no PS text anywhere
|
||||||
|
8 app-shell geometry at 390px: no horizontal overflow, controls + ps-card in view
|
||||||
|
Writes audit/ps-auto-service/*.png + checks.json, prints PS-AUTO-AUDIT-OK/FAIL.
|
||||||
|
"""
|
||||||
|
import json, os, re, sys
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
OUT = 'audit/ps-auto-service'
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
CHROME = '/usr/bin/google-chrome'
|
||||||
|
BASE = os.environ.get('AUDIT_BASE', 'http://127.0.0.1:8801/')
|
||||||
|
LS = 'retire-planner-v1'
|
||||||
|
|
||||||
|
BASE_A = {
|
||||||
|
"province": "ON", "age": 45, "retireAge": 65, "household": "single",
|
||||||
|
"partner": {"age": 45, "income": 0, "rrsp": 0, "tfsa": 0, "nonreg": 0, "monthlyContribution": 0},
|
||||||
|
"income": 60000, "rrsp": 100000, "tfsa": 30000, "nonreg": 20000,
|
||||||
|
"monthlyContribution": 500, "employerMatchMonthly": 0,
|
||||||
|
"spendingAnnual": 50000, "cppStartAge": 65, "oasStartAge": 65,
|
||||||
|
"pensionAnnual": 0, "rentalAnnual": 0, "otherAnnual": 0,
|
||||||
|
"psPension": True, "psJoinYear": 2005, "psServiceOverride": None, "psAvgSalary": 108324,
|
||||||
|
"psOccupation": "it", "psItLevel": "IT-03", "psItStep": 3,
|
||||||
|
"realReturnPre": 3.5, "realReturnPost": 2.5, "inflation": 2.0, "lifeExp": 95,
|
||||||
|
"interests": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
GEO_JS = """() => {
|
||||||
|
const de = document.scrollingElement;
|
||||||
|
const r = (s) => { const e = document.querySelector(s); if (!e) return null; const b = e.getBoundingClientRect();
|
||||||
|
return {top: Math.round(b.top), bottom: Math.round(b.bottom), left: Math.round(b.left), right: Math.round(b.right), w: Math.round(b.width)}; };
|
||||||
|
return { iw: innerWidth, docScrollW: de.scrollWidth,
|
||||||
|
f1: r('#f1'), next: r('#btn-next'), uc: r('#ps-usecalc'), card: r('#ps-card') };
|
||||||
|
}"""
|
||||||
|
|
||||||
|
results, fails = [], 0
|
||||||
|
def ok(name, cond, extra=''):
|
||||||
|
global fails
|
||||||
|
results.append({'check': name, 'pass': bool(cond), 'extra': str(extra)[:180]})
|
||||||
|
if not cond: fails += 1
|
||||||
|
print((' PASS ' if cond else ' FAIL ') + name + ((' [' + str(extra)[:150] + ']') if extra and not cond else ''))
|
||||||
|
|
||||||
|
def nb(s):
|
||||||
|
return s.replace('\u00a0', ' ').replace('\u202f', ' ')
|
||||||
|
|
||||||
|
def seed(page, a, step, screen, lang):
|
||||||
|
page.evaluate("""([ls, st]) => localStorage.setItem(ls, JSON.stringify(st))""", (LS, {'a': a, 'step': step, 'screen': screen, 'lang': lang}))
|
||||||
|
page.reload(wait_until='load')
|
||||||
|
page.wait_for_timeout(250)
|
||||||
|
|
||||||
|
def geo_checks(tag, g, need_card=False):
|
||||||
|
ok(tag + ': no horizontal overflow at 390px', g['docScrollW'] <= g['iw'] + 1, g)
|
||||||
|
if g['f1']:
|
||||||
|
ok(tag + ': #f1 in viewport', g['f1']['top'] >= 0 and g['f1']['bottom'] <= g['iw'] * 3, g['f1'])
|
||||||
|
if g['next']:
|
||||||
|
ok(tag + ': #btn-next in viewport', 0 <= g['next']['top'] and g['next']['bottom'] <= 844, g['next'])
|
||||||
|
if need_card:
|
||||||
|
ok(tag + ': ps-card fits the viewport', g['card'] and g['card']['right'] <= g['iw'] + 1 and g['card']['left'] >= 0, g['card'])
|
||||||
|
|
||||||
|
with sync_playwright() as pw:
|
||||||
|
browser = pw.chromium.launch(executable_path=CHROME, args=['--no-sandbox'])
|
||||||
|
page = browser.new_page(viewport={'width': 390, 'height': 844})
|
||||||
|
page.goto(BASE, wait_until='load')
|
||||||
|
|
||||||
|
# ---- EN 1: auto prefill, untouched ----
|
||||||
|
seed(page, dict(BASE_A), 13, 'quiz', 'en')
|
||||||
|
v = page.input_value('#f1')
|
||||||
|
hint = nb(page.inner_text('#step-body .hint'))
|
||||||
|
dis = page.is_disabled('#btn-next')
|
||||||
|
uc_vis = page.is_visible('#ps-svc-reset')
|
||||||
|
ok('EN auto: input prefilled 41 (age 45, joined 2005, retire 65) — untouched', v == '41', v)
|
||||||
|
ok('EN auto: derivation hint', re.search(r'Calculated: joined 2005 at age 24, retiring at 65 . 41 years of service', hint), hint)
|
||||||
|
ok('EN auto: Next enabled + Use-calculated hidden', dis is False and uc_vis is False, f'disabled={dis} uc_visible={uc_vis}')
|
||||||
|
geo_checks('EN-auto-step', page.evaluate(GEO_JS))
|
||||||
|
page.screenshot(path=OUT + '/en-auto-step.png')
|
||||||
|
|
||||||
|
# ---- EN 2: real typing → override (hint must LIVE-swap, no re-render in a real browser) ----
|
||||||
|
page.fill('#f1', '30')
|
||||||
|
page.wait_for_timeout(150)
|
||||||
|
hint2 = nb(page.inner_text('#step-body .hint'))
|
||||||
|
st = page.evaluate("ls => JSON.parse(localStorage.getItem(ls))", LS)
|
||||||
|
ok('EN override (real typing): hint LIVE-swaps to override + auto', re.search(r'Using your value \(30 years\).*gives 41 years', hint2, re.S), hint2)
|
||||||
|
ok('EN override: "Use calculated" button becomes visible', page.is_visible('#ps-svc-reset'),
|
||||||
|
page.evaluate("() => document.querySelector('#ps-svc-reset').style.display"))
|
||||||
|
ok('EN override: persisted psServiceOverride = 30', st['a'].get('psServiceOverride') == 30, st['a'].get('psServiceOverride'))
|
||||||
|
geo_checks('EN-override-step', page.evaluate(GEO_JS))
|
||||||
|
page.screenshot(path=OUT + '/en-override-step.png')
|
||||||
|
|
||||||
|
# ---- EN 3: use-calc restores auto ----
|
||||||
|
page.click('#ps-usecalc')
|
||||||
|
page.wait_for_timeout(250)
|
||||||
|
v3 = page.input_value('#f1')
|
||||||
|
hint3 = nb(page.inner_text('#step-body .hint'))
|
||||||
|
st3 = page.evaluate("ls => JSON.parse(localStorage.getItem(ls))", LS)
|
||||||
|
ok('EN use-calc: prefill back to 41 + auto hint', v3 == '41' and 'Calculated: joined 2005' in hint3, f'{v3} | {hint3[:90]}')
|
||||||
|
ok('EN use-calc: override cleared in localStorage', st3['a'].get('psServiceOverride') is None, st3['a'].get('psServiceOverride'))
|
||||||
|
ok('EN use-calc: button hidden again', page.is_visible('#ps-svc-reset') is False)
|
||||||
|
|
||||||
|
# ---- EN 4/5: results reflect override vs auto ----
|
||||||
|
seed(page, dict(BASE_A, psServiceOverride=30), 0, 'results', 'en')
|
||||||
|
card = nb(page.inner_text('#ps-card'))
|
||||||
|
ok('EN results (override 30): 30 years used', re.search(r'30 years \(max 35\)', card), card[:160])
|
||||||
|
ok('EN results (override 30): from-65 51,006.90', '51,006.90' in card, card[:160])
|
||||||
|
geo_checks('EN-results', page.evaluate(GEO_JS), need_card=True)
|
||||||
|
page.screenshot(path=OUT + '/en-results-override.png')
|
||||||
|
|
||||||
|
seed(page, dict(BASE_A), 0, 'results', 'en')
|
||||||
|
card2 = nb(page.inner_text('#ps-card'))
|
||||||
|
ok('EN results (auto): 35 years used (auto 41, cap 35)', re.search(r'35 years \(max 35\)', card2), card2[:160])
|
||||||
|
ok('EN results (auto): from-65 59,508.05', '59,508.05' in card2, card2[:160])
|
||||||
|
page.screenshot(path=OUT + '/en-results-auto.png')
|
||||||
|
|
||||||
|
# ---- FR 6: wording ----
|
||||||
|
seed(page, dict(BASE_A), 13, 'quiz', 'fr')
|
||||||
|
vf = page.input_value('#f1')
|
||||||
|
hintF = nb(page.inner_text('#step-body .hint'))
|
||||||
|
ok('FR auto: prefilled 41', vf == '41', vf)
|
||||||
|
ok('FR auto: derivation hint', re.search(r'Calcul. : entr.e en poste en 2005 . 24 ans, d.part . 65 ans . 41 ann.es de service', hintF), hintF)
|
||||||
|
page.fill('#f1', '30')
|
||||||
|
page.wait_for_timeout(150)
|
||||||
|
hintF2 = nb(page.inner_text('#step-body .hint'))
|
||||||
|
ucf = page.query_selector('#ps-usecalc')
|
||||||
|
ok('FR override: hint', re.search(r'Votre valeur \(30 ann.es\) est utilis.e.*donne 41 ans', hintF2, re.S), hintF2)
|
||||||
|
ok('FR override: "Utiliser le calcul" visible', page.is_visible('#ps-svc-reset') and 'calcul' in ucf.inner_text().lower(),
|
||||||
|
page.evaluate("""() => { const e = document.querySelector('#ps-svc-reset');
|
||||||
|
const cs = getComputedStyle(e), b = e.getBoundingClientRect();
|
||||||
|
return { display: cs.display, vis: cs.visibility, box: [b.width, b.height],
|
||||||
|
lang: document.documentElement.lang }; }"""))
|
||||||
|
page.screenshot(path=OUT + '/fr-override-step.png')
|
||||||
|
seed(page, dict(BASE_A), 0, 'results', 'fr')
|
||||||
|
cardF = nb(page.inner_text('#ps-card'))
|
||||||
|
ok('FR results (auto): 35 années (max 35)', re.search(r'35 ann.es \(max 35\)', cardF), cardF[:160])
|
||||||
|
ok('FR results (auto): 59 508,05 formatted fr-CA', re.search(r'59\s?508,05', cardF), cardF[:160])
|
||||||
|
seed(page, dict(BASE_A, psServiceOverride=30), 0, 'results', 'fr')
|
||||||
|
cardFO = nb(page.inner_text('#ps-card'))
|
||||||
|
ok('FR results (override): 30 années + 51 006,90', re.search(r'30 ann.es \(max 35\)', cardFO) and re.search(r'51\s?006,90', cardFO), cardFO[:200])
|
||||||
|
geo_checks('FR-results', page.evaluate(GEO_JS), need_card=True)
|
||||||
|
page.screenshot(path=OUT + '/fr-results-override.png')
|
||||||
|
|
||||||
|
# ---- 7: module OFF parity (15 steps, no PS text) ----
|
||||||
|
seed(page, dict(BASE_A, psPension=False, psJoinYear=None, psServiceOverride=None, psAvgSalary=None,
|
||||||
|
psOccupation=None, psItLevel=None, psItStep=0), 14, 'quiz', 'en')
|
||||||
|
sc = nb(page.inner_text('#step-count'))
|
||||||
|
ok('module OFF: 15 steps (last = 15/15)', re.search(r'15\s*/\s*15|15 of 15|of 15', sc), sc)
|
||||||
|
page.click('#btn-next')
|
||||||
|
page.wait_for_timeout(400)
|
||||||
|
body_all = nb(page.inner_text('body')).lower()
|
||||||
|
# parity: no PS QUIZ content (the static results footer "…pension (PSSA) modelled when enabled" predates this change)
|
||||||
|
ok('module OFF: no PS quiz text on results, card hidden',
|
||||||
|
('pensionable' not in body_all and 'years of pensionable' not in body_all and
|
||||||
|
page.evaluate("() => document.querySelector('#ps-card').hidden") is True),
|
||||||
|
[w for w in ('pensionable', 'years of pensionable service') if w in body_all])
|
||||||
|
page.screenshot(path=OUT + '/module-off-results.png')
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
with open(OUT + '/checks.json', 'w') as f:
|
||||||
|
json.dump(results, f, indent=1)
|
||||||
|
print(f"\n{len(results)} checks, {fails} failed")
|
||||||
|
print('PS-AUTO-AUDIT-OK' if fails == 0 else 'PS-AUTO-AUDIT-FAIL')
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
retireAge: 65, spendingAnnual: 50000,
|
retireAge: 65, spendingAnnual: 50000,
|
||||||
cppStartAge: 65, oasStartAge: 65,
|
cppStartAge: 65, oasStartAge: 65,
|
||||||
pensionAnnual: 0, rentalAnnual: 0, otherAnnual: 0,
|
pensionAnnual: 0, rentalAnnual: 0, otherAnnual: 0,
|
||||||
psPension: false, psJoinYear: null, psServiceYears: null, psAvgSalary: null,
|
psPension: false, psJoinYear: null, psServiceYears: null, psServiceOverride: null, psAvgSalary: null,
|
||||||
psOccupation: null, psItLevel: null, psItStep: 0,
|
psOccupation: null, psItLevel: null, psItStep: 0,
|
||||||
realReturnPre: 3.5, realReturnPost: 2.5, inflation: 2.0, lifeExp: 95,
|
realReturnPre: 3.5, realReturnPost: 2.5, inflation: 2.0, lifeExp: 95,
|
||||||
interests: []
|
interests: []
|
||||||
@@ -65,6 +65,11 @@
|
|||||||
if (m) { try { raw = decodeURIComponent(escape(atob(m[1]))); } catch (e) { raw = null; } }
|
if (m) { try { raw = decodeURIComponent(escape(atob(m[1]))); } catch (e) { raw = null; } }
|
||||||
if (!raw) { try { raw = localStorage.getItem(LS); } catch (e) { raw = null; } }
|
if (!raw) { try { raw = localStorage.getItem(LS); } catch (e) { raw = null; } }
|
||||||
if (raw) { try { var o = JSON.parse(raw); var b = Object.assign({}, o.a || o); delete b.lang; Object.assign(state.a, b); state.step = o.step | 0; if (o.screen) state.screen = o.screen; else if (m) state.screen = 'results'; } catch (e) { /* fresh */ } }
|
if (raw) { try { var o = JSON.parse(raw); var b = Object.assign({}, o.a || o); delete b.lang; Object.assign(state.a, b); state.step = o.step | 0; if (o.screen) state.screen = o.screen; else if (m) state.screen = 'results'; } catch (e) { /* fresh */ } }
|
||||||
|
/* Migration (PS-AUTO-SERVICE): legacy saved psServiceYears was always user-typed →
|
||||||
|
treat it as the override so pre-auto-derivation plans compute exactly as before. */
|
||||||
|
if (state.a.psServiceOverride == null && typeof state.a.psServiceYears === 'number' && isFinite(state.a.psServiceYears)) {
|
||||||
|
state.a.psServiceOverride = state.a.psServiceYears;
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
function save() {
|
function save() {
|
||||||
var t2 = { a: state.a, step: state.step, screen: state.screen, lang: getLang() };
|
var t2 = { a: state.a, step: state.step, screen: state.screen, lang: getLang() };
|
||||||
@@ -106,6 +111,24 @@
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
function psOn() { return !!state.a.psPension; }
|
function psOn() { return !!state.a.psPension; }
|
||||||
|
/* Auto-derived pensionable service (PS-AUTO-SERVICE): retirement age − age at join,
|
||||||
|
from current age + join year. Null when inputs are missing/invalid. */
|
||||||
|
var PS_CUR_YEAR = new Date().getFullYear();
|
||||||
|
function psAutoServiceYears() {
|
||||||
|
if (typeof PS === 'undefined' || !PS.autoServiceYears) return null;
|
||||||
|
return PS.autoServiceYears(state.a.age, state.a.psJoinYear, state.a.retireAge, PS_CUR_YEAR);
|
||||||
|
}
|
||||||
|
/* EFFECTIVE service years — the single contract the engine + summary lines must use:
|
||||||
|
user override when set, else the auto derivation (null = not derivable yet). */
|
||||||
|
function psServiceEffective() {
|
||||||
|
var o = state.a.psServiceOverride;
|
||||||
|
if (o != null && isFinite(+o)) return +o;
|
||||||
|
return psAutoServiceYears();
|
||||||
|
}
|
||||||
|
function psOverrideActive() {
|
||||||
|
var o = state.a.psServiceOverride;
|
||||||
|
return o != null && isFinite(+o) && psAutoServiceYears() != null;
|
||||||
|
}
|
||||||
function psAugment(p) { /* re-derive PS pension for a scenario's retireAge */
|
function psAugment(p) { /* re-derive PS pension for a scenario's retireAge */
|
||||||
var ps = psCompute(p.retireAge);
|
var ps = psCompute(p.retireAge);
|
||||||
if (ps) { p.pensionAnnual = ps.pensionAtRetirement; p.pensionStartAge = ps.startAge; p.pensionBridgeAnnual = ps.bridge; }
|
if (ps) { p.pensionAnnual = ps.pensionAtRetirement; p.pensionStartAge = ps.startAge; p.pensionBridgeAnnual = ps.bridge; }
|
||||||
@@ -118,7 +141,9 @@
|
|||||||
if (!avg) return null;
|
if (!avg) return null;
|
||||||
return PS.computePSPension({
|
return PS.computePSPension({
|
||||||
joinYear: +a.psJoinYear || 0, retireAge: retireAge,
|
joinYear: +a.psJoinYear || 0, retireAge: retireAge,
|
||||||
serviceYears: +a.psServiceYears || 0, avgSalary: avg,
|
/* EFFECTIVE value: override when set, else auto-derivation (never null while
|
||||||
|
the module is computable — PS-AUTO-SERVICE). */
|
||||||
|
serviceYears: +psServiceEffective() || 0, avgSalary: avg,
|
||||||
ampe: (typeof CANADA !== 'undefined' && CANADA.ympe && CANADA.ympe.value) || 74600
|
ampe: (typeof CANADA !== 'undefined' && CANADA.ympe && CANADA.ympe.value) || 74600
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -215,8 +240,24 @@
|
|||||||
get: function () { return state.a.psJoinYear; }, set: function (v) { state.a.psJoinYear = +v; }
|
get: function () { return state.a.psJoinYear; }, set: function (v) { state.a.psJoinYear = +v; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ps_service', show: psOn, titleK: 'q.ps_service.title', hintK: 'q.ps_service.hint', type: 'number', min: 0, max: 45,
|
key: 'ps_service', show: psOn, titleK: 'q.ps_service.title',
|
||||||
get: function () { return state.a.psServiceYears; }, set: function (v) { state.a.psServiceYears = +v; }
|
hint: function () {
|
||||||
|
var auto = psAutoServiceYears(), o = state.a.psServiceOverride;
|
||||||
|
if (o != null && isFinite(+o)) {
|
||||||
|
return t('q.ps_service.override', { years: fmtInt(+o), auto: auto == null ? '—' : fmtInt(auto) });
|
||||||
|
}
|
||||||
|
if (auto != null) {
|
||||||
|
return t('q.ps_service.auto', {
|
||||||
|
join: state.a.psJoinYear,
|
||||||
|
joinAge: fmtInt(+state.a.age - (PS_CUR_YEAR - +state.a.psJoinYear)),
|
||||||
|
ret: state.a.retireAge, years: fmtInt(auto)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return t('q.ps_service.hint');
|
||||||
|
},
|
||||||
|
type: 'number', min: 0, max: 60, /* auto-derivation clamps to 60 (engine caps 35) */
|
||||||
|
get: function () { var e = psServiceEffective(); return e == null ? '' : e; },
|
||||||
|
set: function (v) { state.a.psServiceOverride = (v === '' || v == null || isNaN(+v)) ? null : +v; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ps_salary', show: psOn, titleK: 'q.ps_salary.title',
|
key: 'ps_salary', show: psOn, titleK: 'q.ps_salary.title',
|
||||||
@@ -314,9 +355,10 @@
|
|||||||
btnNext.textContent = state.step === vs.length - 1 ? t('quiz.seePlan') : t('quiz.next');
|
btnNext.textContent = state.step === vs.length - 1 ? t('quiz.seePlan') : t('quiz.next');
|
||||||
|
|
||||||
var hintTxt = typeof s.hint === 'function' ? s.hint() : (s.hintK ? t(s.hintK) : '');
|
var hintTxt = typeof s.hint === 'function' ? s.hint() : (s.hintK ? t(s.hintK) : '');
|
||||||
|
var hintAttr = s.key === 'ps_service' ? ' id="ps-hint"' : ''; /* PS-AUTO-SERVICE: live-swap hook */
|
||||||
var titleTxt = typeof s.titleK === 'function' ? s.titleK() : t(s.titleK);
|
var titleTxt = typeof s.titleK === 'function' ? s.titleK() : t(s.titleK);
|
||||||
var optList = typeof s.opts === 'function' ? s.opts() : s.opts;
|
var optList = typeof s.opts === 'function' ? s.opts() : s.opts;
|
||||||
var h = '<h2>' + titleTxt + '</h2>' + (hintTxt ? '<p class="hint">' + hintTxt + '</p>' : '');
|
var h = '<h2>' + titleTxt + '</h2>' + (hintTxt ? '<p class="hint"' + hintAttr + '>' + hintTxt + '</p>' : '');
|
||||||
|
|
||||||
if (s.type === 'grid') {
|
if (s.type === 'grid') {
|
||||||
h += '<div class="opts">' + optList.map(function (o) {
|
h += '<div class="opts">' + optList.map(function (o) {
|
||||||
@@ -334,6 +376,12 @@
|
|||||||
}).join('') + '</div>';
|
}).join('') + '</div>';
|
||||||
} else if (s.type === 'number') {
|
} else if (s.type === 'number') {
|
||||||
h += '<div class="fld"><input type="number" id="f1" value="' + (s.get() == null ? '' : s.get()) + '" min="' + (s.min != null ? s.min : 0) + '" max="' + (s.max != null ? s.max : 1e9) + '" inputmode="numeric" ' + (s.money ? 'placeholder="0"' : 'placeholder="' + fmtInt(s.max) + '"') + '></div>';
|
h += '<div class="fld"><input type="number" id="f1" value="' + (s.get() == null ? '' : s.get()) + '" min="' + (s.min != null ? s.min : 0) + '" max="' + (s.max != null ? s.max : 1e9) + '" inputmode="numeric" ' + (s.money ? 'placeholder="0"' : 'placeholder="' + fmtInt(s.max) + '"') + '></div>';
|
||||||
|
/* PS-AUTO-SERVICE: when the user has overridden the auto-derived service, offer the
|
||||||
|
CPP-"Use maximum"-style reset back to the calculated value. Always in the DOM so
|
||||||
|
oninput can reveal/hide it live (real browsers only re-render on step change). */
|
||||||
|
if (s.key === 'ps_service') {
|
||||||
|
h += '<p class="hint" id="ps-svc-reset" style="margin:8px 0 0' + (psOverrideActive() ? '' : ';display:none') + '"><button type="button" class="link" id="ps-usecalc">' + t('q.ps_service.useCalc') + '</button></p>';
|
||||||
|
}
|
||||||
} else if (s.type === 'fields') {
|
} else if (s.type === 'fields') {
|
||||||
h += s.fields.map(function (f, i) {
|
h += s.fields.map(function (f, i) {
|
||||||
if (f.key === 'pensionAnnual' && psOn()) {
|
if (f.key === 'pensionAnnual' && psOn()) {
|
||||||
@@ -369,11 +417,23 @@
|
|||||||
});
|
});
|
||||||
var f1 = $('#f1');
|
var f1 = $('#f1');
|
||||||
if (f1) {
|
if (f1) {
|
||||||
f1.oninput = function () { s.set(f1.value); save(); liveUpdate(); validate(s); if (s.type === 'slider') $('#sv').textContent = s.fmt(+f1.value); };
|
f1.oninput = function () {
|
||||||
|
s.set(f1.value); save(); liveUpdate(); validate(s);
|
||||||
|
if (s.type === 'slider') $('#sv').textContent = s.fmt(+f1.value);
|
||||||
|
/* PS-AUTO-SERVICE: real browsers keep the rendered hint on typing (no re-render until
|
||||||
|
step change) — swap the derivation hint + reveal the reset button live. */
|
||||||
|
if (s.key === 'ps_service') {
|
||||||
|
var he = $('#ps-hint'); if (he) he.textContent = s.hint();
|
||||||
|
var rb = $('#ps-svc-reset'); if (rb) rb.style.display = psOverrideActive() ? '' : 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
if (s.type === 'slider') f1.oninput();
|
if (s.type === 'slider') f1.oninput();
|
||||||
/* Fix 8: Enter on a number step = press Next (when enabled). */
|
/* Fix 8: Enter on a number step = press Next (when enabled). */
|
||||||
if (s.type === 'number') f1.onkeydown = function (e) { if (e.key === 'Enter' && !btnNext.disabled) btnNext.click(); };
|
if (s.type === 'number') f1.onkeydown = function (e) { if (e.key === 'Enter' && !btnNext.disabled) btnNext.click(); };
|
||||||
}
|
}
|
||||||
|
/* PS-AUTO-SERVICE: "Use calculated" clears the override → auto value flows to engine + summary. */
|
||||||
|
var uc = $('#ps-usecalc');
|
||||||
|
if (uc) uc.onclick = function () { state.a.psServiceOverride = null; save(); liveUpdate(); renderStep(); };
|
||||||
stepBody.querySelectorAll('input[type=number][data-k]').forEach(function (inp) {
|
stepBody.querySelectorAll('input[type=number][data-k]').forEach(function (inp) {
|
||||||
inp.oninput = function () { s.set(inp.dataset.k, inp.value); save(); liveUpdate(); validate(s); };
|
inp.oninput = function () { s.set(inp.dataset.k, inp.value); save(); liveUpdate(); validate(s); };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,6 +87,25 @@
|
|||||||
return y <= GROUP1_CUTOFF_YEAR ? 1 : 2;
|
return y <= GROUP1_CUTOFF_YEAR ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Auto-derive years of pensionable service (PS-AUTO-SERVICE):
|
||||||
|
* yearsWorkedSoFar = currentYear − joinYear
|
||||||
|
* ageAtJoin = age − yearsWorkedSoFar
|
||||||
|
* serviceYears = retireAge − ageAtJoin, clamped to [0, 60] (whole years;
|
||||||
|
* the pension formula applies its own 35-yr MAX_SERVICE cap).
|
||||||
|
* currentYear defaults to the calendar year. Returns null when age, joinYear or
|
||||||
|
* retireAge is missing/invalid — the UI then falls back to manual entry. */
|
||||||
|
function autoServiceYears(age, joinYear, retireAge, currentYear) {
|
||||||
|
var y = +currentYear;
|
||||||
|
if (!(y > 0)) y = new Date().getFullYear();
|
||||||
|
var a = +age, j = +joinYear, r = +retireAge;
|
||||||
|
if (!(a > 0) || !(r > 0) || !(y > 0)) return null;
|
||||||
|
if (!(j >= 1900 && j <= 2999)) return null;
|
||||||
|
if (!isFinite(a) || !isFinite(j) || !isFinite(r) || !isFinite(y)) return null;
|
||||||
|
var joinAge = a - (y - j);
|
||||||
|
var s = r - joinAge;
|
||||||
|
return Math.round(Math.max(0, Math.min(60, s)));
|
||||||
|
}
|
||||||
|
|
||||||
function itLevel(id) {
|
function itLevel(id) {
|
||||||
for (var i = 0; i < IT_LEVELS.length; i++) if (IT_LEVELS[i].id === id) return IT_LEVELS[i];
|
for (var i = 0; i < IT_LEVELS.length; i++) if (IT_LEVELS[i].id === id) return IT_LEVELS[i];
|
||||||
return null;
|
return null;
|
||||||
@@ -194,7 +213,7 @@
|
|||||||
SOURCES: SOURCES,
|
SOURCES: SOURCES,
|
||||||
psGroup: psGroup, psLifetimePension: psLifetimePension,
|
psGroup: psGroup, psLifetimePension: psLifetimePension,
|
||||||
psBridgeBenefit: psBridgeBenefit, psEarlyReduction: psEarlyReduction,
|
psBridgeBenefit: psBridgeBenefit, psEarlyReduction: psEarlyReduction,
|
||||||
computePSPension: computePSPension,
|
computePSPension: computePSPension, autoServiceYears: autoServiceYears,
|
||||||
itLevel: itLevel, itRange: itRange
|
itLevel: itLevel, itRange: itRange
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -122,7 +122,10 @@
|
|||||||
},
|
},
|
||||||
ps_service: {
|
ps_service: {
|
||||||
title: 'Years of pensionable service',
|
title: 'Years of pensionable service',
|
||||||
hint: 'Recognized by PSPC (the calculation caps it at 35 years).'
|
hint: 'Recognized by PSPC (the calculation caps it at 35 years).',
|
||||||
|
auto: 'Calculated: joined {join} at age {joinAge}, retiring at {ret} → {years} years of service. Adjust only if you bought past service.',
|
||||||
|
override: 'Using your value ({years} years). The calculation from your join year gives {auto} years.',
|
||||||
|
useCalc: 'Use calculated'
|
||||||
},
|
},
|
||||||
ps_salary: {
|
ps_salary: {
|
||||||
title: 'Average salary — your 5 consecutive highest-paid years',
|
title: 'Average salary — your 5 consecutive highest-paid years',
|
||||||
@@ -379,7 +382,10 @@
|
|||||||
},
|
},
|
||||||
ps_service: {
|
ps_service: {
|
||||||
title: 'Années de service ouvrant droit à pension',
|
title: 'Années de service ouvrant droit à pension',
|
||||||
hint: 'Reconnues par la PSCC (le calcul est plafonné à 35 années).'
|
hint: 'Reconnues par la PSCC (le calcul est plafonné à 35 années).',
|
||||||
|
auto: 'Calculé : entrée en poste en {join} à {joinAge} ans, départ à {ret} ans → {years} années de service. À ajuster seulement si vous avez acheté des services antérieurs.',
|
||||||
|
override: 'Votre valeur ({years} années) est utilisée. Le calcul à partir de votre année d\'entrée donne {auto} ans.',
|
||||||
|
useCalc: 'Utiliser le calcul'
|
||||||
},
|
},
|
||||||
ps_salary: {
|
ps_salary: {
|
||||||
title: 'Salaire moyen — vos 5 années consécutives les mieux rémunérées',
|
title: 'Salaire moyen — vos 5 années consécutives les mieux rémunérées',
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ button,input{touch-action:manipulation}
|
|||||||
#step-body{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:18px;min-height:320px}
|
#step-body{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:18px;min-height:320px}
|
||||||
#step-body h2{margin-top:.1em}
|
#step-body h2{margin-top:.1em}
|
||||||
.hint{font-size:13.5px;color:var(--ink2)}
|
.hint{font-size:13.5px;color:var(--ink2)}
|
||||||
|
.link{background:none;border:0;padding:0;font:inherit;font-weight:700;color:var(--blue);cursor:pointer;text-decoration:underline;text-underline-offset:2px} /* text-button (e.g. "Use calculated" reset on the auto-derived service step) */
|
||||||
.fld{margin:14px 0}
|
.fld{margin:14px 0}
|
||||||
.fld label{display:block;font-weight:600;font-size:15px;margin-bottom:5px}
|
.fld label{display:block;font-weight:600;font-size:15px;margin-bottom:5px}
|
||||||
.fld .unit{font-size:13px;color:var(--ink2)}
|
.fld .unit{font-size:13px;color:var(--ink2)}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -15,7 +15,17 @@ function makeEl(id) {
|
|||||||
id, _html: '', _l: {}, style: {}, dataset: {}, hidden: false,
|
id, _html: '', _l: {}, style: {}, dataset: {}, hidden: false,
|
||||||
value: '', textContent: '', disabled: false,
|
value: '', textContent: '', disabled: false,
|
||||||
get innerHTML() { return this._html; },
|
get innerHTML() { return this._html; },
|
||||||
set innerHTML(v) { this._html = String(v); },
|
set innerHTML(v) {
|
||||||
|
this._html = String(v);
|
||||||
|
/* shim ext (PS-AUTO-SERVICE): a real DOM creates a fresh <input> with the attribute value
|
||||||
|
on every innerHTML write; the registry reuses one #f1 element, so its stale .value let a
|
||||||
|
slider's initial oninput() wipe fields (retireAge → 0) and poisoned later slider steps.
|
||||||
|
Sync #f1.value from the rendered value="…" attribute, exactly like the browser. */
|
||||||
|
if (id === 'step-body' && typeof registry !== 'undefined' && registry.f1) {
|
||||||
|
const m = /<input[^>]*id="f1"[^>]*value="([^"]*)"/.exec(this._html);
|
||||||
|
registry.f1.value = m ? m[1] : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
addEventListener(t, fn) { this._l[t] = fn; },
|
addEventListener(t, fn) { this._l[t] = fn; },
|
||||||
setAttribute() { }, getAttribute() { return null; },
|
setAttribute() { }, getAttribute() { return null; },
|
||||||
querySelector() { return makeEl(id + '>q'); },
|
querySelector() { return makeEl(id + '>q'); },
|
||||||
@@ -188,6 +198,8 @@ function runPs(lang, M, done) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const card = q('#ps-card');
|
const card = q('#ps-card');
|
||||||
ok('PS results card visible', card.hidden === false);
|
ok('PS results card visible', card.hidden === false);
|
||||||
|
ok('legacy saved psServiceYears → psServiceOverride (pre-auto plans keep their exact numbers)',
|
||||||
|
JSON.parse(store[LS]).a.psServiceOverride === 30, JSON.stringify(JSON.parse(store[LS]).a.psServiceOverride));
|
||||||
const h = N(card._html);
|
const h = N(card._html);
|
||||||
ok('PS card title localized', M.psCard.test(h), h.slice(0, 70));
|
ok('PS card title localized', M.psCard.test(h), h.slice(0, 70));
|
||||||
ok('PS card pension at 60 (2%×100k×30 = 60,000)', M.psAt.test(h), (h.match(M.psAt) || [''])[0]);
|
ok('PS card pension at 60 (2%×100k×30 = 60,000)', M.psAt.test(h), (h.match(M.psAt) || [''])[0]);
|
||||||
@@ -207,7 +219,11 @@ function runPs(lang, M, done) {
|
|||||||
|
|
||||||
/* PS module quiz click-through — regression test (2026-10-09): typing into an initially-empty
|
/* PS module quiz click-through — regression test (2026-10-09): typing into an initially-empty
|
||||||
PS number step (join year / service / salary) MUST re-enable Next. Pre-fix, renderStep validated
|
PS number step (join year / service / salary) MUST re-enable Next. Pre-fix, renderStep validated
|
||||||
only at render, so the button stayed disabled and a real user was STUCK on the join step. */
|
only at render, so the button stayed disabled and a real user was STUCK on the join step.
|
||||||
|
PS-AUTO-SERVICE (2026-10-09): the ps_service step now prefills itself from age + join year +
|
||||||
|
retireAge (defaults: age 45, joined 2005 → joined at 24, retire 65 → auto 41). Walk checks:
|
||||||
|
auto prefill + derivation hint + summary use the auto value; a typed override (30) drives the
|
||||||
|
summary instead; "Use calculated" restores the auto summary. EN + FR. */
|
||||||
function runPsQuiz(lang, done) {
|
function runPsQuiz(lang, done) {
|
||||||
console.log('\n--- ' + lang.toUpperCase() + ' (PS quiz click-through) ---');
|
console.log('\n--- ' + lang.toUpperCase() + ' (PS quiz click-through) ---');
|
||||||
load(lang);
|
load(lang);
|
||||||
@@ -215,6 +231,7 @@ function runPsQuiz(lang, done) {
|
|||||||
const opt = makeEl('opt'); opt.dataset.v = 'ON';
|
const opt = makeEl('opt'); opt.dataset.v = 'ON';
|
||||||
q('#step-body').querySelectorAll = (sel) => (sel === '.opt' ? [opt] : []);
|
q('#step-body').querySelectorAll = (sel) => (sel === '.opt' ? [opt] : []);
|
||||||
click('btn-begin'); // re-render step 0 with patched querySelectorAll
|
click('btn-begin'); // re-render step 0 with patched querySelectorAll
|
||||||
|
const body = () => N(q('#step-body').innerHTML);
|
||||||
const typed = {};
|
const typed = {};
|
||||||
let guard = 0, joinDisabledBefore = null;
|
let guard = 0, joinDisabledBefore = null;
|
||||||
while (guard++ < 40) {
|
while (guard++ < 40) {
|
||||||
@@ -229,7 +246,22 @@ function runPsQuiz(lang, done) {
|
|||||||
ok('PS join step starts disabled (empty field)', joinDisabledBefore === true, 'disabled=' + joinDisabledBefore);
|
ok('PS join step starts disabled (empty field)', joinDisabledBefore === true, 'disabled=' + joinDisabledBefore);
|
||||||
ok('typing into empty PS step enables Next (regression: validate on input)', q('#btn-next').disabled === false, 'after=' + q('#btn-next').disabled);
|
ok('typing into empty PS step enables Next (regression: validate on input)', q('#btn-next').disabled === false, 'after=' + q('#btn-next').disabled);
|
||||||
continue; }
|
continue; }
|
||||||
if (step === 13 && !typed.svc) { typed.svc = 1; const f1 = q('#f1'); f1.value = '30'; f1._l.input && f1._l.input.call(f1); continue; }
|
if (step === 13 && !typed.auto) {
|
||||||
|
typed.auto = 1;
|
||||||
|
/* PS-AUTO-SERVICE: NO typing here — auto = 65 − (45 − (2026 − 2005)) = 41 */
|
||||||
|
ok('ps_service prefilled with the auto value 41 (joined 2005 at 24, retire 65) — no typing',
|
||||||
|
/value="41"/.test(body()));
|
||||||
|
ok('ps_service auto hint shows the derivation',
|
||||||
|
lang === 'en' ? /Calculated: joined 2005 at age 24, retiring at 65 → 41 years of service/.test(body())
|
||||||
|
: /Calculé : entrée en poste en 2005 à 24 ans, départ à 65 ans → 41 années de service/.test(body()),
|
||||||
|
(body().match(/<p class="hint">[^<]*/) || [''])[0]);
|
||||||
|
ok('ps_service Next enabled from the auto value, override stays null',
|
||||||
|
q('#btn-next').disabled === false && JSON.parse(store[LS]).a.psServiceOverride == null,
|
||||||
|
'disabled=' + q('#btn-next').disabled);
|
||||||
|
ok('reset button in DOM but HIDDEN in auto mode',
|
||||||
|
/id="ps-svc-reset"[^>]*display:none/.test(body()) && /id="ps-usecalc"/.test(body()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (step === 14 && !typed.sal) { typed.sal = 1; const f1 = q('#f1'); f1.value = '90000'; f1._l.input && f1._l.input.call(f1); continue; }
|
if (step === 14 && !typed.sal) { typed.sal = 1; const f1 = q('#f1'); f1.value = '90000'; f1._l.input && f1._l.input.call(f1); continue; }
|
||||||
if (step === 15 && !typed.occ) { typed.occ = 1; opt.dataset.v = 'it'; opt._l.click(); continue; }
|
if (step === 15 && !typed.occ) { typed.occ = 1; opt.dataset.v = 'it'; opt._l.click(); continue; }
|
||||||
if (step === 16) {
|
if (step === 16) {
|
||||||
@@ -241,14 +273,72 @@ function runPsQuiz(lang, done) {
|
|||||||
click('btn-next');
|
click('btn-next');
|
||||||
if (q('#scr-results').hidden === false) break;
|
if (q('#scr-results').hidden === false) break;
|
||||||
}
|
}
|
||||||
const a = JSON.parse(store[LS]).a;
|
|
||||||
ok('PS click-through reached results', q('#scr-results').hidden === false, 'guard=' + guard);
|
ok('PS click-through reached results', q('#scr-results').hidden === false, 'guard=' + guard);
|
||||||
ok('PS card rendered after click-through', !!q('#ps-card') && q('#ps-card').hidden === false);
|
ok('PS card rendered after click-through', !!q('#ps-card') && q('#ps-card').hidden === false);
|
||||||
ok('PS state persisted (join 2005 / service 30 / IT-03 step 3)',
|
const cardA = N(q('#ps-card').innerHTML);
|
||||||
a.psPension === true && a.psJoinYear === 2005 && a.psServiceYears === 30 && a.psOccupation === 'it' && a.psItLevel === 'IT-03' && a.psItStep === 3,
|
ok('auto service flows to the summary: 35 years used (auto 41, engine cap 35)',
|
||||||
JSON.stringify({ j: a.psJoinYear, s: a.psServiceYears, o: a.psOccupation, l: a.psItLevel, st: a.psItStep }));
|
lang === 'en' ? /35 years \(max 35\)/.test(cardA) : /35 années \(max 35\)/.test(cardA), cardA.slice(0, 160));
|
||||||
ok('IT step pick pre-filled salary (IT-03 s3 = 108,324)', a.psAvgSalary === 108324, a.psAvgSalary);
|
ok('auto summary amount (S 35, avg 108,324, G1 @65): from-65 59,508.05',
|
||||||
done();
|
lang === 'en' ? /59,508\.05/.test(cardA) : /59[\s\u00a0\u202f]508,05/.test(cardA), cardA.slice(0, 160));
|
||||||
|
setTimeout(() => {
|
||||||
|
/* back to ps_service via the REAL route (results → Edit answers → 6× Back), type an override */
|
||||||
|
click('btn-edit');
|
||||||
|
for (let i = 0; i < 6; i++) click('btn-back');
|
||||||
|
let st = JSON.parse(store[LS]);
|
||||||
|
ok('back-navigation lands on ps_service (step 13)', st.step === 13, 'step=' + st.step);
|
||||||
|
ok('ps_service shows the auto value again (override null)', /value="41"/.test(body()) && st.a.psServiceOverride == null);
|
||||||
|
const f1b = q('#f1'); f1b.value = '30'; f1b._l.input && f1b._l.input.call(f1b);
|
||||||
|
click('btn-back'); click('btn-next'); // re-render ps_service with the override in place
|
||||||
|
st = JSON.parse(store[LS]);
|
||||||
|
ok('override typed → psServiceOverride = 30 (the value the engine must use)',
|
||||||
|
st.a.psServiceOverride === 30, JSON.stringify(st.a.psServiceOverride));
|
||||||
|
const bodyO = body();
|
||||||
|
ok('override hint states the override + the auto value',
|
||||||
|
lang === 'en' ? /Using your value \(30 years\).*gives 41 years/.test(bodyO)
|
||||||
|
: /Votre valeur \(30 années\) est utilisée.*donne 41 ans/.test(bodyO),
|
||||||
|
(bodyO.match(/<p class="hint">[^<]*/) || [''])[0]);
|
||||||
|
ok('"Use calculated" button VISIBLE (not hidden) while overridden',
|
||||||
|
/id="ps-svc-reset" style="margin:8px 0 0">/.test(bodyO) && /id="ps-usecalc"/.test(bodyO),
|
||||||
|
(bodyO.match(/id="ps-svc-reset"[^>]*>/) || [''])[0]);
|
||||||
|
let g2 = 0;
|
||||||
|
while (g2++ < 12 && q('#scr-results').hidden !== false) {
|
||||||
|
if (q('#btn-next').disabled) break;
|
||||||
|
click('btn-next');
|
||||||
|
}
|
||||||
|
const cardB = N(q('#ps-card').innerHTML);
|
||||||
|
ok('override flows to the summary: 30 years used',
|
||||||
|
lang === 'en' ? /30 years \(max 35\)/.test(cardB) : /30 années \(max 35\)/.test(cardB), cardB.slice(0, 160));
|
||||||
|
ok('override summary amount (S 30): from-65 51,006.90',
|
||||||
|
lang === 'en' ? /51,006\.90/.test(cardB) : /51[\s\u00a0\u202f]006,90/.test(cardB), cardB.slice(0, 160));
|
||||||
|
setTimeout(() => {
|
||||||
|
click('btn-edit');
|
||||||
|
for (let i = 0; i < 6; i++) click('btn-back');
|
||||||
|
ok('back on ps_service with the override (button shown)',
|
||||||
|
JSON.parse(store[LS]).step === 13 && /id="ps-svc-reset" style="margin:8px 0 0">/.test(body()));
|
||||||
|
const uc = q('#ps-usecalc');
|
||||||
|
ok('"Use calculated" handler wired', !!uc && !!uc._l.click);
|
||||||
|
uc._l.click.call(uc);
|
||||||
|
st = JSON.parse(store[LS]);
|
||||||
|
ok('"Use calculated" clears the override → auto again', st.a.psServiceOverride === null, JSON.stringify(st.a.psServiceOverride));
|
||||||
|
ok('auto value prefilled + auto hint restored',
|
||||||
|
/value="41"/.test(body()) && (lang === 'en' ? /Calculated: joined 2005/.test(body()) : /Calculé : entrée en poste en 2005/.test(body())));
|
||||||
|
let g3 = 0;
|
||||||
|
while (g3++ < 12 && q('#scr-results').hidden !== false) {
|
||||||
|
if (q('#btn-next').disabled) break;
|
||||||
|
click('btn-next');
|
||||||
|
}
|
||||||
|
const cardC = N(q('#ps-card').innerHTML);
|
||||||
|
ok('use-calc restores the auto summary (35 years · 59,508.05)',
|
||||||
|
(lang === 'en' ? /35 years \(max 35\)/.test(cardC) : /35 années \(max 35\)/.test(cardC)) &&
|
||||||
|
(lang === 'en' ? /59,508\.05/.test(cardC) : /59[\s\u00a0\u202f]508,05/.test(cardC)), cardC.slice(0, 160));
|
||||||
|
const a = JSON.parse(store[LS]).a;
|
||||||
|
ok('PS state persisted (join 2005 / no override / IT-03 step 3, salary 108,324)',
|
||||||
|
a.psPension === true && a.psJoinYear === 2005 && a.psServiceOverride === null &&
|
||||||
|
a.psOccupation === 'it' && a.psItLevel === 'IT-03' && a.psItStep === 3 && a.psAvgSalary === 108324,
|
||||||
|
JSON.stringify({ j: a.psJoinYear, o: a.psOccupation, l: a.psItLevel, s: a.psItStep, sal: a.psAvgSalary }));
|
||||||
|
done();
|
||||||
|
}, 400);
|
||||||
|
}, 400);
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||