// Builds dataset wa-cares-premiums-vs-benefit: tables (CSV) + saved-analysis results (JSON). // Model: WA Cares premium 0.58% of all wages (no cap, RCW 50B.04.080 and 50B.04.010(16)); // maximum lifetime benefit 365 benefit units of $100 = $36,500 (RCW 50B.04.010(3), 50B.04.060(3)(b)), indexed to CPI. // Economic assumptions are Milliman's 2024 actuarial valuation ultimate rates: wage growth 3.55%, // benefit indexing (CPI) 2.4%, net investment earned rate 4.0%. // All dollar values are in 2026 dollars (deflated by the 2.4% benefit index), so the benefit stays $36,500. // Run: node build.js (writes ../data/*.csv and ../data/results.json) const fs = require('fs'), path = require('path'); const out = path.join(__dirname, '..', 'data'); fs.mkdirSync(out, { recursive: true }); const RATE = 0.0058, BEN = 36500, CPI = 0.024, WAGEG = 0.0355, NIER = 0.04; const realOf = nom => (1 + nom) / (1 + CPI) - 1; const realR = realOf(NIER), realG = realOf(WAGEG); // One worker's career: avgWage = career-average annual wage in 2026 dollars. // profile 'flat' = same real wage every year; 'rising' = grows at realG, scaled to the same average. // claimLag = years from the first premium to the year the whole benefit is used. function career({ avgWage, years = 40, claimLag = 60, profile = 'flat', disc = realR }) { let w = []; for (let t = 0; t < years; t++) w.push(profile === 'flat' ? 1 : Math.pow(1 + realG, t)); const mean = w.reduce((a, b) => a + b, 0) / years; w = w.map(x => x * avgWage / mean); return { total: w.reduce((a, b) => a + b * RATE, 0), pv: w.reduce((a, b, t) => a + b * RATE / Math.pow(1 + disc, t), 0), pvBen: BEN / Math.pow(1 + disc, claimLag), }; } function breakEven(opts, key) { let lo = 1000, hi = 5e6; for (let i = 0; i < 100; i++) { const m = (lo + hi) / 2, r = career({ ...opts, avgWage: m }); const prem = key === 'total' ? r.total : r.pv, ben = key === 'total' ? BEN : r.pvBen; if (prem > ben) hi = m; else lo = m; } return Math.round((lo + hi) / 2 / 100) * 100; } const r0 = x => Math.round(x), r2 = x => Math.round(x * 100) / 100; const csv = rows => { const c = Object.keys(rows[0]); const e = v => { const s = v === null ? '' : String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; return c.join(',') + '\n' + rows.map(r => c.map(k => e(r[k])).join(',')).join('\n') + '\n'; }; // Table 1: career premiums by career-average wage, 40-year career (ages 25-64), benefit used at 85. const grid = []; for (let w = 20000; w <= 300000; w += 10000) grid.push(w); const byWage = grid.map(w => { const r = career({ avgWage: w }); return { career_average_wage: w, career_years: 40, total_premiums: r0(r.total), pv_premiums_at_4pct: r0(r.pv), max_benefit: BEN, pv_max_benefit_at_85: r0(r.pvBen), benefit_to_premium_ratio: r2(BEN / r.total), pv_benefit_to_pv_premium_ratio: r2(r.pvBen / r.pv), }; }); // Table 2: break-even wages under alternative assumptions. const SC = [ ['base', 'Base: 40-year career (ages 25-64), flat real wage, benefit used at 85, 4.0% discount rate', { years: 40, claimLag: 60 }, NIER], ['claim-80', 'Benefit used at 80 instead of 85', { years: 40, claimLag: 55 }, NIER], ['claim-75', 'Benefit used at 75 instead of 85', { years: 40, claimLag: 50 }, NIER], ['rising-wage', 'Wage rises 1.12% a year faster than prices over the career (same career average)', { years: 40, claimLag: 60, profile: 'rising' }, NIER], ['disc-3', 'Discount rate 3.0% instead of 4.0%', { years: 40, claimLag: 60, disc: realOf(0.03) }, 0.03], ['disc-5', 'Discount rate 5.0% instead of 4.0%', { years: 40, claimLag: 60, disc: realOf(0.05) }, 0.05], ['career-30', '30 years of premiums (age 35 in 2026, works through 64), benefit used at 85', { years: 30, claimLag: 50 }, NIER], ['career-20', '20 years of premiums (age 45 in 2026, works through 64), benefit used at 85', { years: 20, claimLag: 40 }, NIER], ['career-10', '10 years of premiums (age 55 in 2026, works through 64), benefit used at 85', { years: 10, claimLag: 30 }, NIER], ]; const scen = SC.map(([id, label, o, nom]) => ({ scenario_id: id, scenario: label, premium_years: o.years, years_until_benefit_used: o.claimLag, nominal_discount_rate: nom, break_even_wage_simple: breakEven(o, 'total'), break_even_wage_present_value: breakEven(o, 'pv'), })); scen.push({ scenario_id: 'near-retiree', scenario: 'Born before 1968: each year of premiums earns 10% of the maximum benefit ($3,650)', premium_years: 1, years_until_benefit_used: null, nominal_discount_rate: null, break_even_wage_simple: Math.round(BEN * 0.1 / RATE / 100) * 100, break_even_wage_present_value: null, }); // Table 3: Washington wage distribution (BLS OEWS, May 2025, all occupations) and the 40-year outcome at each point. const OES = [['p10', '10th percentile', 38670], ['p25', '25th percentile', 47020], ['p50', 'Median', 62990], ['mean', 'Mean', 83330], ['p75', '75th percentile', 101710], ['p90', '90th percentile', 153630]]; const dist = OES.map(([id, label, w]) => { const r = career({ avgWage: w }); return { point_id: id, point: label, annual_wage_may_2025: w, total_premiums_40yr: r0(r.total), pv_premiums_40yr: r0(r.pv), max_benefit: BEN, pv_max_benefit_at_85: r0(r.pvBen), pays_more_than_max_benefit_simple: r.total > BEN ? 'yes' : 'no', pays_more_than_pv_max_benefit: r.pv > r.pvBen ? 'yes' : 'no', }; }); // Table 4: program parameters. const params = [ ['premium-rate', 'Premium rate', '0.58% of wages; from 2026 the Pension Funding Council may set a lower rate but not a higher one', 'RCW 50B.04.080(1)', 'rcw-50b04'], ['wage-cap', 'Wage cap', 'None: all wages are subject to the premium', 'RCW 50B.04.010(16)', 'rcw-50b04'], ['collection-start', 'Premium collection began', '2023-07-01, after the 2022 Legislature delayed it from 2022-01-01', 'RCW 50B.04.080(1); SHB 1732 (2022)', 'shb-1732'], ['benefit-unit', 'Benefit unit', 'Up to $100 of approved services on a given day, adjusted each January 1 by the Seattle-area CPI-W', 'RCW 50B.04.010(3)', 'rcw-50b04'], ['lifetime-max', 'Lifetime maximum', '365 benefit units ($36,500 as of July 2026); units can be combined, so there is no daily limit', 'RCW 50B.04.060(3)(b)', 'rcw-50b04'], ['vesting', 'Full vesting', '10 years of premiums, or 3 of the last 6 years, with at least 500 hours worked in each of those years', 'RCW 50B.04.050(1),(3)', 'rcw-50b04'], ['near-retiree', 'Near-retirees (born before 1968)', '10% of the maximum benefit for each year of premiums', 'RCW 50B.04.050(2)', 'rcw-50b04'], ['benefits-start', 'Benefits payable', 'In Washington from 2026-07-01; for out-of-state participants from 2030-07-01', 'RCW 50B.04.060(1),(2)', 'rcw-50b04'], ['portability', 'Moving out of state', 'From 2026-07-01, a person with 3 years of premiums can elect, within a year of moving, to keep paying premiums on all wages and stay covered', 'RCW 50B.04.180', 'rcw-50b04'], ['private-exemption', 'Private-insurance exemption', 'Open 2021-10-01 to 2022-12-31 to holders of long-term care insurance bought before 2021-11-01; 480,366 approved; may be rescinded before 2028-07-01', 'RCW 50B.04.085', 'esd-exemptions'], ['other-exemptions', 'Other voluntary exemptions (from 2023)', 'Veterans with a 70% or greater service-connected disability, military spouses and partners, active-duty members with civilian jobs, employees living outside Washington, temporary-visa holders', 'RCW 50B.04.053, 50B.04.055', 'rcw-50b04'], ].map(([id, parameter, value, citation, src]) => ({ parameter_id: id, parameter, value, legal_citation: citation, source_id: src })); // Chart rows (long format): one row per wage level and series. const chartRows = []; for (const r of byWage) { chartRows.push({ career_average_wage: r.career_average_wage, series: 'Career premiums, inflation-adjusted', dollars: r.total_premiums }); chartRows.push({ career_average_wage: r.career_average_wage, series: 'Career premiums, present value at 4%', dollars: r.pv_premiums_at_4pct }); chartRows.push({ career_average_wage: r.career_average_wage, series: 'Maximum benefit', dollars: r.max_benefit }); chartRows.push({ career_average_wage: r.career_average_wage, series: 'Maximum benefit, present value if used at 85', dollars: r.pv_max_benefit_at_85 }); } const files = { 'career-premiums-by-wage.csv': byWage, 'break-even-scenarios.csv': scen, 'wa-wage-distribution-outcomes.csv': dist, 'program-parameters.csv': params, }; for (const [f, rows] of Object.entries(files)) fs.writeFileSync(path.join(out, f), csv(rows)); fs.writeFileSync(path.join(out, 'results.json'), JSON.stringify({ assumptions: { RATE, BEN, CPI, WAGEG, NIER, realR, realG }, chartRows, byWage, scen, dist, params }, null, 1) + '\n'); console.log(JSON.stringify(scen)); console.log(JSON.stringify(dist));