social-security-calculator 2.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/constants.d.ts +3 -1
- package/lib/constants.js +5 -2
- package/lib/index.d.ts +3 -6
- package/lib/index.js +168 -77
- package/lib/model.d.ts +6 -0
- package/lib/wage-index.js +15 -9
- package/package.json +3 -2
package/lib/constants.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const EARLY_RETIREMENT_AGE = 62;
|
|
2
|
-
export declare const WAGE_INDEX_CUTOFF =
|
|
2
|
+
export declare const WAGE_INDEX_CUTOFF = 2024;
|
|
3
3
|
export declare const MAX_RETIREMENT_AGE = 70;
|
|
4
4
|
export declare const LOOKBACK_YEARS = 40;
|
|
5
5
|
export declare const MAX_DROP_OUT_YEARS = 5;
|
|
@@ -8,7 +8,9 @@ export declare const ELAPSED_YEARS_START_AGE = 22;
|
|
|
8
8
|
export declare const BEND_POINT_DIVISOR = 9779.44;
|
|
9
9
|
export declare const FIRST_BEND_POINT_MULTIPLIER = 180;
|
|
10
10
|
export declare const SECOND_BEND_POINT_MULTIPLIER = 1085;
|
|
11
|
+
export declare const FAM_MAX_BASES: number[];
|
|
11
12
|
export declare const CHILD_SURVIVOR_BENEFIT_PERCENTAGE = 0.75;
|
|
13
|
+
export declare const DISABILITY_LAST_EARNINGS_YEARS_THRESHOLD = 5;
|
|
12
14
|
export declare const PIA_PERCENTAGES: {
|
|
13
15
|
readonly FIRST_BRACKET: 0.9;
|
|
14
16
|
readonly SECOND_BRACKET: 0.32;
|
package/lib/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Constants
|
|
2
2
|
export const EARLY_RETIREMENT_AGE = 62;
|
|
3
|
-
export const WAGE_INDEX_CUTOFF =
|
|
3
|
+
export const WAGE_INDEX_CUTOFF = 2024;
|
|
4
4
|
export const MAX_RETIREMENT_AGE = 70;
|
|
5
5
|
// LOOKBACK_YEARS This represents the working years between 22 and 62,
|
|
6
6
|
// but it does not filter out the wages of years before and after that range.
|
|
@@ -13,7 +13,10 @@ export const ELAPSED_YEARS_START_AGE = 22;
|
|
|
13
13
|
export const BEND_POINT_DIVISOR = 9779.44; // 1977's AWI - used by dividing against current AWI
|
|
14
14
|
export const FIRST_BEND_POINT_MULTIPLIER = 180;
|
|
15
15
|
export const SECOND_BEND_POINT_MULTIPLIER = 1085;
|
|
16
|
+
export const FAM_MAX_BASES = [230, 332, 433];
|
|
16
17
|
export const CHILD_SURVIVOR_BENEFIT_PERCENTAGE = 0.75; // 75% of PIA for child survivor benefits
|
|
18
|
+
// Disability 5-year rule: if disability date is more than 5 years after last earnings year, disability benefit is zero
|
|
19
|
+
export const DISABILITY_LAST_EARNINGS_YEARS_THRESHOLD = 5;
|
|
17
20
|
export const PIA_PERCENTAGES = {
|
|
18
21
|
FIRST_BRACKET: 0.9,
|
|
19
22
|
SECOND_BRACKET: 0.32,
|
|
@@ -21,6 +24,6 @@ export const PIA_PERCENTAGES = {
|
|
|
21
24
|
};
|
|
22
25
|
export const EARLY_RETIREMENT_REDUCTION = {
|
|
23
26
|
FIRST_MONTHS: 36,
|
|
24
|
-
FIRST_MONTHS_RATE: 5 / 9 * 0.01,
|
|
27
|
+
FIRST_MONTHS_RATE: 5 / 9 * 0.01, // 5/9 of 1%
|
|
25
28
|
ADDITIONAL_MONTHS_RATE: 5 / 12 * 0.01 // 5/12 of 1%
|
|
26
29
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { BenefitCalculationResult,
|
|
1
|
+
import { BenefitCalculationResult, Earnings } from './model';
|
|
2
2
|
export declare function calc(birthday: Date, retirementDate: Date, earnings: Earnings): BenefitCalculationResult;
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function calculateRetirementDates(birthday: Date, retirementDate: Date): RetirementDates;
|
|
3
|
+
export declare function calcRetirement(birthday: Date, retirementDate: Date, earnings: Earnings): Partial<BenefitCalculationResult>;
|
|
5
4
|
export declare function calculatePIA(AIME: number, baseYear?: number): number;
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function calculateAIME(earnings: Earnings, yearStartCounter: number, effectiveDay: Date, baseYear?: number): number;
|
|
5
|
+
export declare function calculateAIME(earnings: Earnings, lookbackYears: number, baseYear?: number): number;
|
|
8
6
|
export declare function getEnglishCommonLawDate(date: Date): Date;
|
|
9
|
-
export declare function getFullRetirementMonths(commonLawBirthDate: Date): number;
|
package/lib/index.js
CHANGED
|
@@ -1,57 +1,89 @@
|
|
|
1
1
|
import { wageIndex } from './wage-index';
|
|
2
|
-
import { EARLY_RETIREMENT_AGE, WAGE_INDEX_CUTOFF, MAX_RETIREMENT_AGE, MAX_DROP_OUT_YEARS, DROP_OUT_YEARS_DIVISOR, BEND_POINT_DIVISOR, FIRST_BEND_POINT_MULTIPLIER, SECOND_BEND_POINT_MULTIPLIER, PIA_PERCENTAGES, EARLY_RETIREMENT_REDUCTION, ELAPSED_YEARS_START_AGE, LOOKBACK_YEARS, CHILD_SURVIVOR_BENEFIT_PERCENTAGE, } from './constants';
|
|
3
|
-
// Main
|
|
2
|
+
import { EARLY_RETIREMENT_AGE, WAGE_INDEX_CUTOFF, MAX_RETIREMENT_AGE, MAX_DROP_OUT_YEARS, DROP_OUT_YEARS_DIVISOR, BEND_POINT_DIVISOR, FIRST_BEND_POINT_MULTIPLIER, SECOND_BEND_POINT_MULTIPLIER, PIA_PERCENTAGES, EARLY_RETIREMENT_REDUCTION, ELAPSED_YEARS_START_AGE, LOOKBACK_YEARS, CHILD_SURVIVOR_BENEFIT_PERCENTAGE, FAM_MAX_BASES, DISABILITY_LAST_EARNINGS_YEARS_THRESHOLD } from './constants';
|
|
3
|
+
// Main entry point
|
|
4
4
|
export function calc(birthday, retirementDate, earnings) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const retirementCalc = calcRetirement(birthday, retirementDate, earnings);
|
|
6
|
+
const survivorCalc = calcSurvivor(birthday, retirementDate, earnings);
|
|
7
|
+
retirementCalc.SurvivorBenefits = survivorCalc;
|
|
8
|
+
return retirementCalc;
|
|
9
|
+
}
|
|
10
|
+
// Core retirement calculation
|
|
11
|
+
export function calcRetirement(birthday, retirementDate, earnings) {
|
|
12
|
+
const context = createCalculationContext(birthday, retirementDate, earnings);
|
|
13
|
+
const regularBenefit = calculateBenefitPipeline(earnings, context.yearAge60, getLookbackYears(context.totalYears));
|
|
14
|
+
const disabilityBenefit = calculateBenefitPipeline(earnings, context.yearAge60, getLookbackYearsDisability(context.totalYears));
|
|
15
|
+
const monthlyBenefit = retirementDateAdjustedPayment(context.dates, regularBenefit.colaAdjustedPIA);
|
|
16
|
+
// Apply disability 5-year rule: if disability date is more than 5 years after last earnings year, benefit is zero
|
|
17
|
+
let disabilityPIAFloored = 0;
|
|
18
|
+
if (context.retirementDate <= context.dates.fullRetirement && !isDisabilityDateBeyondFiveYearRule(retirementDate, earnings)) {
|
|
19
|
+
disabilityPIAFloored = Math.floor(disabilityBenefit.colaAdjustedPIA);
|
|
8
20
|
}
|
|
21
|
+
return {
|
|
22
|
+
AIME: regularBenefit.aime,
|
|
23
|
+
PIA: regularBenefit.pia,
|
|
24
|
+
NormalMonthlyBenefit: monthlyBenefit,
|
|
25
|
+
DisabilityEarnings: disabilityPIAFloored,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Reusable benefit calculation pipeline
|
|
29
|
+
function calculateBenefitPipeline(earnings, yearAge60, lookbackYears) {
|
|
30
|
+
const aime = calculateAIME(earnings, lookbackYears, yearAge60);
|
|
31
|
+
const pia = calculatePIA(aime, yearAge60);
|
|
32
|
+
const colaAdjustedPIA = calculateCOLAAdjustments(pia, yearAge60 + 2);
|
|
33
|
+
return { aime, pia, colaAdjustedPIA };
|
|
34
|
+
}
|
|
35
|
+
// Survivor benefits calculation
|
|
36
|
+
function calcSurvivor(birthday, retirementDate, earnings) {
|
|
37
|
+
const context = createCalculationContext(birthday, retirementDate, earnings);
|
|
38
|
+
const benefit = calculateBenefitPipeline(earnings, context.yearAge60, getLookbackYears(context.totalYears));
|
|
39
|
+
const survivorPIA = Math.floor(benefit.colaAdjustedPIA * CHILD_SURVIVOR_BENEFIT_PERCENTAGE);
|
|
40
|
+
const effectiveYear = Math.min(context.yearAge60, WAGE_INDEX_CUTOFF);
|
|
41
|
+
const wageIndexEntry = getWageIndexEntry(effectiveYear);
|
|
42
|
+
const wageIndexLastYear = wageIndexEntry.awi;
|
|
43
|
+
const familyMax = calculateFamilyMaximum(benefit.pia, getFamilyMaxBendPoints(wageIndexLastYear));
|
|
44
|
+
const colaAdjustedFamMax = calculateCOLAAdjustments(familyMax, context.yearAge60 + 2);
|
|
45
|
+
return {
|
|
46
|
+
survivingChild: survivorPIA,
|
|
47
|
+
careGivingSpouse: survivorPIA,
|
|
48
|
+
normalRetirementSpouse: Math.floor(benefit.colaAdjustedPIA),
|
|
49
|
+
familyMaximum: colaAdjustedFamMax
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Create shared calculation context to avoid duplication
|
|
53
|
+
function createCalculationContext(birthday, retirementDate, earnings) {
|
|
54
|
+
validateInputs(birthday, retirementDate, earnings);
|
|
9
55
|
const dates = calculateRetirementDates(birthday, retirementDate);
|
|
10
|
-
if (!earnings || earnings.length === 0) {
|
|
11
|
-
throw new Error('Earnings history cannot be empty');
|
|
12
|
-
}
|
|
13
|
-
if (retirementDate < birthday) {
|
|
14
|
-
throw new Error('Retirement date cannot be before birthday');
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* An individual's earnings are always indexed to the average wage level two years prior to the year of first eligibility.
|
|
18
|
-
* Thus, for a person retiring at age 62 in 2025, the person's earnings would be indexed to the average wage index for 2023.
|
|
19
|
-
*/
|
|
20
56
|
const yearAge60 = birthday.getFullYear() + 60;
|
|
21
57
|
const yearStartCounting = birthday.getFullYear() + ELAPSED_YEARS_START_AGE - 1;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
console.log(`Survivor PIA: ${survivorPIA}`);
|
|
27
|
-
console.log(`Primary Insurance Amount: ${retirementDateAdjustedPayment(dates, survivorPIA)}`);
|
|
28
|
-
const colaAdjustedPIA = calculateCOLAAdjustments(primaryInsuranceAmount, age60Year + 2);
|
|
29
|
-
const disabilityPIA = retirementDate > dates.fullRetirement ? 0 : Math.floor(colaAdjustedPIA);
|
|
30
|
-
const results = retirementDateAdjustedPayment(dates, colaAdjustedPIA);
|
|
58
|
+
const dateStartCounting = new Date(birthday);
|
|
59
|
+
dateStartCounting.setFullYear(yearStartCounting);
|
|
60
|
+
const monthsDiff = monthsDifference(retirementDate, dateStartCounting) / 12;
|
|
61
|
+
const totalYears = Math.min(LOOKBACK_YEARS, monthsDiff);
|
|
31
62
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
birthday,
|
|
64
|
+
retirementDate,
|
|
65
|
+
earnings,
|
|
66
|
+
dates,
|
|
67
|
+
yearAge60,
|
|
68
|
+
yearStartCounting,
|
|
69
|
+
dateStartCounting,
|
|
70
|
+
totalYears
|
|
36
71
|
};
|
|
37
72
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (dates.retirementDate < dates.earliestRetirement) {
|
|
43
|
-
adjustedBenefits = 0;
|
|
73
|
+
// Extracted validation logic
|
|
74
|
+
function validateInputs(birthday, retirementDate, earnings) {
|
|
75
|
+
if (!birthday || !retirementDate) {
|
|
76
|
+
throw new Error('Birthday and retirement date are required');
|
|
44
77
|
}
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
if (!earnings || earnings.length === 0) {
|
|
79
|
+
throw new Error('Earnings history cannot be empty');
|
|
47
80
|
}
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
if (retirementDate < birthday) {
|
|
82
|
+
throw new Error('Retirement date cannot be before birthday');
|
|
50
83
|
}
|
|
51
|
-
const monthlyBenefit = Math.floor(adjustedBenefits);
|
|
52
|
-
return monthlyBenefit;
|
|
53
84
|
}
|
|
54
|
-
|
|
85
|
+
// Retirement date calculations
|
|
86
|
+
function calculateRetirementDates(birthday, retirementDate) {
|
|
55
87
|
const eclBirthDate = getEnglishCommonLawDate(birthday);
|
|
56
88
|
const fraMonths = getFullRetirementMonths(eclBirthDate);
|
|
57
89
|
const earliestRetirement = new Date(eclBirthDate.getFullYear() + EARLY_RETIREMENT_AGE, eclBirthDate.getMonth(), eclBirthDate.getDate());
|
|
@@ -67,6 +99,22 @@ export function calculateRetirementDates(birthday, retirementDate) {
|
|
|
67
99
|
retirementDate
|
|
68
100
|
};
|
|
69
101
|
}
|
|
102
|
+
// Benefit amount calculations
|
|
103
|
+
function retirementDateAdjustedPayment(dates, colaAdjustedPIA) {
|
|
104
|
+
const earlyRetireMonths = monthsDifference(dates.adjusted, dates.fullRetirement);
|
|
105
|
+
let adjustedBenefits = colaAdjustedPIA;
|
|
106
|
+
if (dates.retirementDate < dates.earliestRetirement) {
|
|
107
|
+
adjustedBenefits = 0;
|
|
108
|
+
}
|
|
109
|
+
else if (earlyRetireMonths < 0) {
|
|
110
|
+
adjustedBenefits = calculateEarlyRetirementReduction(colaAdjustedPIA, Math.abs(earlyRetireMonths));
|
|
111
|
+
}
|
|
112
|
+
else if (earlyRetireMonths > 0) {
|
|
113
|
+
adjustedBenefits = calculateDelayedRetirementIncrease(dates.eclBirthDate, colaAdjustedPIA, earlyRetireMonths);
|
|
114
|
+
}
|
|
115
|
+
return Math.floor(adjustedBenefits);
|
|
116
|
+
}
|
|
117
|
+
// COLA adjustments
|
|
70
118
|
function calculateCOLAAdjustments(PIA, startYear) {
|
|
71
119
|
const currentYear = new Date().getFullYear();
|
|
72
120
|
const colaRates = wageIndex
|
|
@@ -77,12 +125,10 @@ function calculateCOLAAdjustments(PIA, startYear) {
|
|
|
77
125
|
return roundToFloorTenCents(adjustedAmount * multiplier);
|
|
78
126
|
}, PIA);
|
|
79
127
|
}
|
|
128
|
+
// PIA calculation
|
|
80
129
|
export function calculatePIA(AIME, baseYear) {
|
|
81
130
|
const effectiveYear = baseYear ? Math.min(baseYear, WAGE_INDEX_CUTOFF) : WAGE_INDEX_CUTOFF;
|
|
82
|
-
const wageIndexEntry =
|
|
83
|
-
if (!wageIndexEntry) {
|
|
84
|
-
throw new Error(`No wage index data found for year ${effectiveYear}`);
|
|
85
|
-
}
|
|
131
|
+
const wageIndexEntry = getWageIndexEntry(effectiveYear);
|
|
86
132
|
const wageIndexLastYear = wageIndexEntry.awi;
|
|
87
133
|
// Calculate bend points (rounded to nearest dollar per SSA rules)
|
|
88
134
|
const firstBendPoint = Math.round(FIRST_BEND_POINT_MULTIPLIER * wageIndexLastYear / BEND_POINT_DIVISOR);
|
|
@@ -104,23 +150,13 @@ export function calculatePIA(AIME, baseYear) {
|
|
|
104
150
|
}
|
|
105
151
|
return roundToFloorTenCents(monthlyBenefit);
|
|
106
152
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const dropOutYears = Math.min(Math.floor(elapsedYears / DROP_OUT_YEARS_DIVISOR), MAX_DROP_OUT_YEARS);
|
|
110
|
-
const adjustedLookbackYears = elapsedYears - dropOutYears;
|
|
111
|
-
return Math.max(minComputationYears, adjustedLookbackYears);
|
|
112
|
-
}
|
|
113
|
-
export function calculateAIME(earnings, yearStartCounter, effectiveDay, baseYear) {
|
|
114
|
-
const totalYears = Math.min(LOOKBACK_YEARS, effectiveDay.getFullYear() - yearStartCounter);
|
|
115
|
-
const lookbackYears = getLookbackYears(totalYears);
|
|
153
|
+
// AIME calculation
|
|
154
|
+
export function calculateAIME(earnings, lookbackYears, baseYear) {
|
|
116
155
|
if (!earnings || earnings.length === 0) {
|
|
117
156
|
return 0;
|
|
118
157
|
}
|
|
119
158
|
const effectiveYear = baseYear ? Math.min(baseYear, WAGE_INDEX_CUTOFF) : WAGE_INDEX_CUTOFF;
|
|
120
|
-
const wageIndexEntry =
|
|
121
|
-
if (!wageIndexEntry) {
|
|
122
|
-
throw new Error(`No wage index data found for year ${effectiveYear}`);
|
|
123
|
-
}
|
|
159
|
+
const wageIndexEntry = getWageIndexEntry(effectiveYear);
|
|
124
160
|
const wageIndexLastYear = wageIndexEntry.awi;
|
|
125
161
|
const futureYearsFactor = 1;
|
|
126
162
|
// Calculate wage index factors
|
|
@@ -133,15 +169,27 @@ export function calculateAIME(earnings, yearStartCounter, effectiveDay, baseYear
|
|
|
133
169
|
acc[year] = earnings * (wageIndexFactors[year] || futureYearsFactor);
|
|
134
170
|
return acc;
|
|
135
171
|
}, {});
|
|
136
|
-
// Get top
|
|
137
|
-
const
|
|
172
|
+
// Get top years of earnings based on lookback period
|
|
173
|
+
const topYearsEarningsArr = Object.values(adjustedEarnings)
|
|
138
174
|
.sort((a, b) => b - a)
|
|
139
175
|
.slice(0, lookbackYears);
|
|
140
|
-
const totalEarnings =
|
|
176
|
+
const totalEarnings = topYearsEarningsArr.reduce((sum, earnings) => sum + earnings, 0);
|
|
141
177
|
// Calculate AIME (rounded down to next lower dollar)
|
|
142
|
-
|
|
143
|
-
return averageIndexedMonthlyEarnings;
|
|
178
|
+
return Math.floor(totalEarnings / (12 * lookbackYears));
|
|
144
179
|
}
|
|
180
|
+
// Lookback year calculations
|
|
181
|
+
function getLookbackYears(elapsedYears) {
|
|
182
|
+
const minComputationYears = 2;
|
|
183
|
+
const adjustedLookbackYears = Math.floor(elapsedYears) - 5;
|
|
184
|
+
return Math.max(minComputationYears, adjustedLookbackYears);
|
|
185
|
+
}
|
|
186
|
+
function getLookbackYearsDisability(elapsedYears) {
|
|
187
|
+
const minComputationYears = 2;
|
|
188
|
+
const dropOutYears = Math.min(Math.floor(elapsedYears / DROP_OUT_YEARS_DIVISOR), MAX_DROP_OUT_YEARS);
|
|
189
|
+
const adjustedLookbackYears = elapsedYears - dropOutYears;
|
|
190
|
+
return Math.max(minComputationYears, adjustedLookbackYears);
|
|
191
|
+
}
|
|
192
|
+
// Early retirement reduction
|
|
145
193
|
function calculateEarlyRetirementReduction(amount, months) {
|
|
146
194
|
if (months <= 0)
|
|
147
195
|
return amount;
|
|
@@ -156,6 +204,7 @@ function calculateEarlyRetirementReduction(amount, months) {
|
|
|
156
204
|
}
|
|
157
205
|
return amount * (1 - reduction);
|
|
158
206
|
}
|
|
207
|
+
// Delayed retirement increase
|
|
159
208
|
function calculateDelayedRetirementIncrease(birthday, initialAmount, numberOfMonths) {
|
|
160
209
|
if (numberOfMonths <= 0)
|
|
161
210
|
return initialAmount;
|
|
@@ -164,11 +213,11 @@ function calculateDelayedRetirementIncrease(birthday, initialAmount, numberOfMon
|
|
|
164
213
|
const totalIncrease = monthlyRate * numberOfMonths;
|
|
165
214
|
return initialAmount * (1 + totalIncrease);
|
|
166
215
|
}
|
|
216
|
+
// Delayed retirement rate lookup
|
|
167
217
|
function getDelayedRetirementRate(birthYear) {
|
|
168
218
|
if (birthYear < 1933) {
|
|
169
219
|
throw new Error(`Invalid birth year for delayed retirement: ${birthYear}`);
|
|
170
220
|
}
|
|
171
|
-
// Rates based on SSA rules
|
|
172
221
|
if (birthYear <= 1934)
|
|
173
222
|
return 11 / 24 / 100; // 11/24 of 1%
|
|
174
223
|
if (birthYear <= 1936)
|
|
@@ -181,30 +230,19 @@ function getDelayedRetirementRate(birthYear) {
|
|
|
181
230
|
return 5 / 8 / 100; // 5/8 of 1%
|
|
182
231
|
return 2 / 3 / 100; // 2/3 of 1%
|
|
183
232
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return Math.floor(amount * 10) / 10;
|
|
187
|
-
}
|
|
188
|
-
export function getEnglishCommonLawDate(date) {
|
|
189
|
-
// Create a new date to avoid mutating the original
|
|
190
|
-
const eclDate = new Date(date);
|
|
191
|
-
eclDate.setDate(eclDate.getDate() - 1);
|
|
192
|
-
return eclDate;
|
|
193
|
-
}
|
|
194
|
-
export function getFullRetirementMonths(commonLawBirthDate) {
|
|
233
|
+
// Full retirement age calculation
|
|
234
|
+
function getFullRetirementMonths(commonLawBirthDate) {
|
|
195
235
|
const year = commonLawBirthDate.getFullYear();
|
|
196
236
|
if (year <= 1937) {
|
|
197
237
|
return 65 * 12;
|
|
198
238
|
}
|
|
199
239
|
else if (year <= 1942) {
|
|
200
|
-
// Gradual increase from 65 years to 65 years 10 months
|
|
201
240
|
return ((year - 1937) * 2) + (65 * 12);
|
|
202
241
|
}
|
|
203
242
|
else if (year <= 1954) {
|
|
204
243
|
return 66 * 12;
|
|
205
244
|
}
|
|
206
245
|
else if (year <= 1959) {
|
|
207
|
-
// Gradual increase from 66 years to 66 years 10 months
|
|
208
246
|
return ((year - 1954) * 2) + (66 * 12);
|
|
209
247
|
}
|
|
210
248
|
else if (year >= 1960) {
|
|
@@ -214,8 +252,61 @@ export function getFullRetirementMonths(commonLawBirthDate) {
|
|
|
214
252
|
throw new Error(`Invalid birth year: ${year}`);
|
|
215
253
|
}
|
|
216
254
|
}
|
|
255
|
+
/** Compute Family-Max bend points for an eligibility year given AWI_{year-2}. */
|
|
256
|
+
function getFamilyMaxBendPoints(baseYear) {
|
|
257
|
+
const [f1, f2, f3] = FAM_MAX_BASES.map(b => Math.round((b * baseYear / BEND_POINT_DIVISOR)));
|
|
258
|
+
return [f1, f2, f3];
|
|
259
|
+
}
|
|
260
|
+
/** Apply the family-maximum formula to a PIA using already-computed bend points. */
|
|
261
|
+
function calculateFamilyMaximum(pia, [bp1, bp2, bp3]) {
|
|
262
|
+
let max = 0;
|
|
263
|
+
max += 1.50 * Math.min(pia, bp1);
|
|
264
|
+
if (pia > bp1)
|
|
265
|
+
max += 2.72 * (Math.min(pia, bp2) - bp1);
|
|
266
|
+
if (pia > bp2)
|
|
267
|
+
max += 1.34 * (Math.min(pia, bp3) - bp2);
|
|
268
|
+
if (pia > bp3)
|
|
269
|
+
max += 1.75 * (pia - bp3);
|
|
270
|
+
// SSA: round total down to next lower $0.10
|
|
271
|
+
return Math.floor(max * 10) / 10;
|
|
272
|
+
}
|
|
273
|
+
// Check if disability date violates the 5-year rule
|
|
274
|
+
function isDisabilityDateBeyondFiveYearRule(disabilityDate, earnings) {
|
|
275
|
+
if (!earnings || earnings.length === 0) {
|
|
276
|
+
return true; // No earnings means no disability benefit
|
|
277
|
+
}
|
|
278
|
+
// Find the last year with non-zero earnings
|
|
279
|
+
const earningsWithValue = earnings.filter(e => e.earnings > 0);
|
|
280
|
+
if (earningsWithValue.length === 0) {
|
|
281
|
+
return true; // No non-zero earnings means no disability benefit
|
|
282
|
+
}
|
|
283
|
+
const lastEarningsYear = Math.max(...earningsWithValue.map(e => e.year));
|
|
284
|
+
// Last day of the last earnings year
|
|
285
|
+
const lastDayOfLastEarningsYear = new Date(lastEarningsYear, 11, 31); // December 31st
|
|
286
|
+
// Add exactly 5 years to the last day of last earnings year
|
|
287
|
+
const fiveYearsLater = new Date(lastDayOfLastEarningsYear);
|
|
288
|
+
fiveYearsLater.setFullYear(fiveYearsLater.getFullYear() + DISABILITY_LAST_EARNINGS_YEARS_THRESHOLD);
|
|
289
|
+
// Disability date must be after the 5-year threshold
|
|
290
|
+
return disabilityDate > fiveYearsLater;
|
|
291
|
+
}
|
|
292
|
+
// Utility functions
|
|
293
|
+
export function getEnglishCommonLawDate(date) {
|
|
294
|
+
const eclDate = new Date(date);
|
|
295
|
+
eclDate.setDate(eclDate.getDate() - 1);
|
|
296
|
+
return eclDate;
|
|
297
|
+
}
|
|
217
298
|
function monthsDifference(date1, date2) {
|
|
218
299
|
const months1 = date1.getFullYear() * 12 + date1.getMonth();
|
|
219
300
|
const months2 = date2.getFullYear() * 12 + date2.getMonth();
|
|
220
301
|
return months1 - months2;
|
|
221
302
|
}
|
|
303
|
+
function roundToFloorTenCents(amount) {
|
|
304
|
+
return Math.floor(amount * 10) / 10;
|
|
305
|
+
}
|
|
306
|
+
function getWageIndexEntry(effectiveYear) {
|
|
307
|
+
const wageIndexEntry = wageIndex.find(val => val.year === effectiveYear);
|
|
308
|
+
if (!wageIndexEntry) {
|
|
309
|
+
throw new Error(`No wage index data found for year ${effectiveYear}`);
|
|
310
|
+
}
|
|
311
|
+
return wageIndexEntry;
|
|
312
|
+
}
|
package/lib/model.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface BenefitCalculationResult {
|
|
|
13
13
|
PIA: number;
|
|
14
14
|
NormalMonthlyBenefit: number;
|
|
15
15
|
DisabilityEarnings: number;
|
|
16
|
+
SurvivorBenefits: {
|
|
17
|
+
survivingChild: number;
|
|
18
|
+
careGivingSpouse: number;
|
|
19
|
+
normalRetirementSpouse: number;
|
|
20
|
+
familyMaximum: number;
|
|
21
|
+
};
|
|
16
22
|
}
|
|
17
23
|
export interface RetirementDates {
|
|
18
24
|
earliestRetirement: Date;
|
package/lib/wage-index.js
CHANGED
|
@@ -442,22 +442,28 @@ export const wageIndex = [
|
|
|
442
442
|
"year": 2024,
|
|
443
443
|
"taxMax": 168600,
|
|
444
444
|
"cola": 2.5,
|
|
445
|
-
"awi":
|
|
445
|
+
"awi": 69846.57
|
|
446
446
|
},
|
|
447
447
|
{
|
|
448
448
|
"year": 2025,
|
|
449
449
|
"taxMax": 176100,
|
|
450
|
-
"cola": 2.
|
|
451
|
-
"awi":
|
|
450
|
+
"cola": 2.8,
|
|
451
|
+
"awi": 72644.64
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"year": 2026,
|
|
455
|
+
"taxMax": 176100,
|
|
456
|
+
"cola": 2.5,
|
|
457
|
+
"awi": 75670.13
|
|
452
458
|
}
|
|
453
459
|
];
|
|
460
|
+
// 2026 60000.00 60000.00 0.00 0.00 176100 2.5 75670.13
|
|
454
461
|
export const wageIndexFuture = [
|
|
455
|
-
{ year:
|
|
456
|
-
{ year:
|
|
457
|
-
{ year:
|
|
458
|
-
{ year:
|
|
459
|
-
{ year:
|
|
460
|
-
{ year: 2029, awi: 84736.18 },
|
|
462
|
+
{ year: 2025, awi: 72644.64 },
|
|
463
|
+
{ year: 2026, awi: 75670.13 },
|
|
464
|
+
{ year: 2027, awi: 78726.01 },
|
|
465
|
+
{ year: 2028, awi: 81961.66 },
|
|
466
|
+
{ year: 2029, awi: 85192.51 },
|
|
461
467
|
{ year: 2030, awi: 88030.45 },
|
|
462
468
|
{ year: 2031, awi: 91479.46 },
|
|
463
469
|
{ year: 2032, awi: 95090.94 },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-security-calculator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Calculate estimated Social Security Benefits",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@jest/globals": "^29.7.0",
|
|
42
42
|
"@types/jest": "^29.5.8",
|
|
43
|
+
"@types/minimist": "^1.2.5",
|
|
43
44
|
"@types/xml2js": "^0.4.11",
|
|
44
45
|
"compound-calc": "^4.0.3",
|
|
45
46
|
"csvtojson": "^2.0.10",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"minimist": "^1.2.8",
|
|
48
49
|
"playwright": "^1.54.1",
|
|
49
50
|
"ts-jest": "^29.1.1",
|
|
50
|
-
"typescript": "^
|
|
51
|
+
"typescript": "^5.9.2"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"xml2js": "^0.6.2"
|