financial-calcs
Version:
Reusable financial calculation library for FERS, Social Security, retirement savings, and mortgage amortization
141 lines • 5.69 kB
JavaScript
export function validateSocialSecurityBenefitInput(input) {
const errors = [];
const { startYear, birthYear, claimingAge, averageIncome, averageCOLA, yearsToProject } = input;
if (startYear < 1900)
errors.push({ field: "startYear", message: "Start Year cannot be before 1900" });
if (birthYear < 1900)
errors.push({ field: "birthYear", message: "Birth Year cannot be before 1900" });
if (claimingAge < 62)
errors.push({ field: "claimingAge", message: "Must be at least 62 to claim Social Security benefits" });
if (averageIncome <= 0)
errors.push({ field: "averageIncome", message: "Average income cannot be negative" });
if (averageCOLA < 0)
errors.push({ field: "averageCOLA", message: "Average COLA cannot be negative" });
if (yearsToProject <= 0)
errors.push({ field: "yearsToProject", message: "Must project at least 1 year" });
return errors;
}
// --- Main Projection ---
export function calculateSocialSecurityBenefitProjection(input) {
return calculateSocialSecurityBenefitProjectionWithOverrides({ ...input, yearOverrides: {} });
}
export function calculateSocialSecurityBenefitProjectionWithOverrides(input) {
const { startYear, birthYear, claimingAge, averageIncome, averageCOLA, yearsToProject, yearOverrides = {} } = input;
const errors = validateSocialSecurityBenefitInput(input);
if (errors.length > 0) {
const err = new Error("Social Security Benefits input validation failed");
err.validationErrors = errors;
throw err;
}
const fullRetirementAge = getFullRetirementAge(birthYear);
const claimingYear = birthYear + claimingAge;
// Estimate monthly PIA (Primary Insurance Amount)
// TODO: Refine to use updated estimatePIAWithAIME (but need to pass in an array of incomes)
const estimatedPIA = estimatePIA(averageIncome);
// Adjust for early/late claiming
const reductionOrIncreaseFactor = calculateAdjustmentFactor(claimingAge, fullRetirementAge);
let annualBenefitBase = estimatedPIA * 12 * reductionOrIncreaseFactor;
const rows = [];
for (let i = 0; i < yearsToProject; i++) {
const year = startYear + i;
const age = year - birthYear;
const override = (yearOverrides && yearOverrides[year]) || {};
const hasOverride = override.colaApplied !== undefined;
const isClaiming = year >= claimingYear;
const benefitForYear = isClaiming ? annualBenefitBase : 0;
let colaAppliedThisIteration = 0;
if (i > 0 && isClaiming) {
// Use override for this year's COLA if present, otherwise use averageCOLA
const colaToUse = override.colaApplied ?? averageCOLA;
annualBenefitBase = annualBenefitBase * (1 + colaToUse / 100);
colaAppliedThisIteration = colaToUse;
}
rows.push({
year,
age,
colaApplied: colaAppliedThisIteration,
annualBenefit: Math.round(benefitForYear),
monthlyBenefit: Math.round(benefitForYear / 12),
hasOverride
});
}
return rows;
}
// --- Helpers ---
// SSA-style PIA calculation with 2025 bend points
function estimatePIA(averageIncome) {
const bendPoint1 = 1226;
const bendPoint2 = 7391;
const taxableMax = 176100; // 2025 SSA maximum taxable earnings
// Cap at taxable maximum
const cappedIncome = Math.min(averageIncome, taxableMax);
const monthlyIncome = cappedIncome / 12;
let pia = 0;
if (monthlyIncome <= bendPoint1) {
pia = monthlyIncome * 0.9;
}
else if (monthlyIncome <= bendPoint2) {
pia = bendPoint1 * 0.9 + (monthlyIncome - bendPoint1) * 0.32;
}
else {
pia =
bendPoint1 * 0.9 +
(bendPoint2 - bendPoint1) * 0.32 +
(monthlyIncome - bendPoint2) * 0.15;
}
return pia;
}
function estimatePIAWithAIME(earnings, // array of up to 35 years of indexed annual earnings
bendPoint1 = 1226, bendPoint2 = 7391) {
// Take highest 35 years
const topEarnings = earnings
.sort((a, b) => b - a)
.slice(0, 35);
const totalIndexedEarnings = topEarnings.reduce((sum, yr) => sum + yr, 0);
// AIME = total / 420 months
const aime = Math.floor(totalIndexedEarnings / 420);
// Apply bend points
let pia = 0;
if (aime <= bendPoint1) {
pia = aime * 0.9;
}
else if (aime <= bendPoint2) {
pia = bendPoint1 * 0.9 + (aime - bendPoint1) * 0.32;
}
else {
pia =
bendPoint1 * 0.9 +
(bendPoint2 - bendPoint1) * 0.32 +
(aime - bendPoint2) * 0.15;
}
// SSA truncates to nearest dime
pia = Math.floor(pia * 10) / 10;
return pia;
}
// Adjustment for early/late claiming relative to FRA
function calculateAdjustmentFactor(claimingAge, fra) {
if (claimingAge < fra) {
const monthsEarly = (fra - claimingAge) * 12;
return 1 - monthsEarly * 0.005; // ~0.5% per month early
}
else if (claimingAge > fra) {
const monthsLate = (claimingAge - fra) * 12;
return 1 + monthsLate * 0.0067; // ~0.67% per month late
}
else {
return 1;
}
}
// Full Retirement Age rules
function getFullRetirementAge(birthYear) {
if (birthYear <= 1937)
return 65;
if (birthYear >= 1938 && birthYear <= 1942)
return 65 + (birthYear - 1937) * (2 / 12);
if (birthYear >= 1943 && birthYear <= 1954)
return 66;
if (birthYear >= 1955 && birthYear <= 1959)
return 66 + (birthYear - 1954) * (2 / 12);
return 67; // 1960 and later
}
//# sourceMappingURL=benefit.js.map