UNPKG

financial-calcs

Version:

Reusable financial calculation library for FERS, Social Security, retirement savings, and mortgage amortization

219 lines 9.26 kB
// -------------------------- Types -------------------------- // -------------------------- Validation -------------------------- export function validateFersPensionInput(input) { const errors = []; const { startYear, birthYear, serviceStartYear, serviceEndYear, retirementAge, currentSalary, salaryGrowthRate, high3Salary, yearsToProject, retirementType } = 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 (serviceStartYear < 1900) errors.push({ field: "serviceStartYear", message: "Service Start Year cannot be before 1900" }); if (serviceEndYear < 1900) errors.push({ field: "serviceEndYear", message: "Service End Year cannot be before 1900" }); if (retirementAge < 40 || retirementAge > 80) errors.push({ field: "retirementAge", message: "Retirement Age must be between 40 and 80" }); if (yearsToProject <= 0) errors.push({ field: "yearsToProject", message: "Must project at least 1 year" }); if (currentSalary <= 0) errors.push({ field: "currentSalary", message: "Salary cannot be negative" }); if (salaryGrowthRate < -100) errors.push({ field: "salaryGrowthRate", message: "Growth rate cannot be less than -100%" }); const serviceStartAge = serviceStartYear - birthYear; if (serviceStartAge < 16) errors.push({ field: "serviceStartYear", message: "Must be at least 16 to start federal job" }); const yearsOfService = retirementAge - (serviceStartYear - birthYear); const minimumServiceYear = getMinimumServiceYear(birthYear, retirementAge, retirementType); if (minimumServiceYear === 0) errors.push({ field: "retirementType", message: "Not eligible to retire with pension" }); if (yearsOfService < minimumServiceYear) errors.push({ field: "serviceStartYear", message: `Must serve at least ${minimumServiceYear} years for ${retirementType} retirement` }); if (retirementType === "deferred" && high3Salary <= 0) errors.push({ field: "high3Salary", message: "High-3 salary must be provided for deferred retirement" }); return errors; } // -------------------------- Main Projection -------------------------- export function calculateFersPensionProjection(input) { return calculateFersPensionProjectionWithOverrides({ ...input, yearOverrides: {} }); } export function calculateFersPensionProjectionWithOverrides(input) { const errors = validateFersPensionInput(input); if (errors.length > 0) { const err = new Error("FERS pension input validation failed"); err.validationErrors = errors; throw err; } const { startYear, birthYear, retirementAge, yearsToProject, retirementType, colaPercent: defaultCola, pensionMultiplier, yearOverrides = {} } = input; const retirementYear = birthYear + retirementAge; const endYear = startYear + yearsToProject; const salaryMap = calculateSalaryHistory(input); const high3 = calculateHigh3(salaryMap, startYear, retirementYear, retirementType, input.high3Salary); const yearsOfService = calculateYearsOfService(input); const pensionReduction = calculatePensionReduction(input, yearsOfService); let pension = high3 * (pensionMultiplier / 100) * yearsOfService * (1 - pensionReduction / 100); const rows = []; for (let year = startYear; year < endYear; year++) { const age = year - birthYear; const override = yearOverrides[year] || {}; const hasOverride = override.salary !== undefined || override.salaryGrowthRate !== undefined || override.colaApplied !== undefined; const row = { year, age, salary: 0, pension: 0, monthlyPension: 0, salaryGrowthRate: 0, colaApplied: 0, hasOverride, }; // Before retirement if (year < retirementYear) { if (retirementType === 'deferred' && year > input.serviceEndYear) { row.salary = 0; row.salaryGrowthRate = 0; } else { row.salary = salaryMap[year] ?? 0; const nextSalary = salaryMap[year + 1]; // Round to 2 decimals row.salaryGrowthRate = Math.round(calculateSalaryGrowthRate(row.salary, nextSalary, override.salaryGrowthRate, input.salaryGrowthRate) * 100) / 100; } } // After retirement else { row.salary = 0; let cola = override.colaApplied ?? defaultCola; if (age >= 63 && year > retirementYear) pension *= 1 + cola / 100; else cola = 0; if (retirementType === 'deferred' && age < 62) cola = 0; row.colaApplied = cola; row.pension = pension; row.monthlyPension = pension / 12; } rows.push(row); } return rows; } // -------------------------- Helpers -------------------------- function calculateSalaryHistory(input) { const { startYear, retirementAge, birthYear, currentSalary, salaryGrowthRate, yearOverrides = {} } = input; const retirementYear = birthYear + retirementAge; const salaryMap = {}; let prevSalary = currentSalary; for (let year = startYear; year < retirementYear; year++) { const override = yearOverrides[year] || {}; const salaryThisYear = override.salary ?? prevSalary; salaryMap[year] = salaryThisYear; const growthToUse = override.salaryGrowthRate ?? salaryGrowthRate; prevSalary = salaryThisYear * (1 + growthToUse / 100); } if (startYear >= retirementYear) salaryMap[startYear] = currentSalary; return salaryMap; } function calculateHigh3(salaryMap, startYear, endYear, retirementType, high3SalaryOverride) { if (high3SalaryOverride !== undefined && retirementType === "deferred") return high3SalaryOverride; const last3 = Object.keys(salaryMap) .map(y => salaryMap[Number(y)]) .slice(-3) .filter((n) => n !== undefined); if (last3.length === 0) return 0; return last3.reduce((sum, s) => sum + s, 0) / Math.min(3, last3.length); } function calculateYearsOfService(input) { const { retirementAge, serviceStartYear, serviceEndYear, birthYear, retirementType } = input; if (retirementType === 'deferred') return serviceEndYear - serviceStartYear; return retirementAge - (serviceStartYear - birthYear); } function calculatePensionReduction(input, yearsOfService) { const { retirementAge, retirementType } = input; let reduction = 0; if (retirementType === 'mra10' || retirementType === 'deferred') { const under62 = Math.max(0, 62 - retirementAge); if (yearsOfService < 30) { if (retirementType === 'deferred' && yearsOfService >= 20 && retirementAge >= 60) reduction = 0; else reduction = 5 * under62; } } return reduction; } function calculateSalaryGrowthRate(currentSalary, nextSalary, overrideGrowth, defaultGrowth) { if (nextSalary !== undefined && currentSalary !== 0 && overrideGrowth === undefined) { return ((nextSalary - currentSalary) / currentSalary) * 100; } return overrideGrowth ?? defaultGrowth ?? 0; } function getMinimumServiceYear(birthYear, retirementAge, retirementType) { const mra = getMRA(birthYear); if (retirementType === 'regular') { if (retirementAge >= 62) return 5; else if (retirementAge >= 60) return 20; else if (retirementAge >= mra) return 30; // Not eligible else return 0; } else if (retirementType === 'mra10') { if (retirementAge >= mra) return 10; // Not eligible else return 0; } else if (retirementType === 'early') { if (retirementAge >= 50) return 20; else return 25; } else if (retirementType == 'deferred') { if (retirementAge >= 62) return 5; else if (retirementAge >= mra) return 10; // Not eligible else return 0; } // Unreachable code return 0; } function getMRA(birthYear) { if (birthYear < 1948) return 55; else if (birthYear == 1948) return 55 + (2 / 12); else if (birthYear == 1949) return 55 + (4 / 12); else if (birthYear == 1950) return 55 + (6 / 12); else if (birthYear == 1951) return 55 + (8 / 12); else if (birthYear == 1952) return 55 + (10 / 12); else if (birthYear <= 1964) return 56; else if (birthYear == 1965) return 56 + (2 / 12); else if (birthYear == 1966) return 56 + (4 / 12); else if (birthYear == 1967) return 56 + (6 / 12); else if (birthYear == 1968) return 56 + (8 / 12); else if (birthYear == 1969) return 56 + (10 / 12); return 57; } //# sourceMappingURL=fers.js.map