social-security-calculator 2.0.0 → 3.0.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.
@@ -8,6 +8,7 @@ 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;
12
13
  export declare const PIA_PERCENTAGES: {
13
14
  readonly FIRST_BRACKET: 0.9;
package/lib/constants.js CHANGED
@@ -13,6 +13,7 @@ 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
17
18
  export const PIA_PERCENTAGES = {
18
19
  FIRST_BRACKET: 0.9,
@@ -21,6 +22,6 @@ export const PIA_PERCENTAGES = {
21
22
  };
22
23
  export const EARLY_RETIREMENT_REDUCTION = {
23
24
  FIRST_MONTHS: 36,
24
- FIRST_MONTHS_RATE: 5 / 9 * 0.01,
25
+ FIRST_MONTHS_RATE: 5 / 9 * 0.01, // 5/9 of 1%
25
26
  ADDITIONAL_MONTHS_RATE: 5 / 12 * 0.01 // 5/12 of 1%
26
27
  };
package/lib/index.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { BenefitCalculationResult, RetirementDates, Earnings } from './model';
2
2
  export declare function calc(birthday: Date, retirementDate: Date, earnings: Earnings): BenefitCalculationResult;
3
- export declare function retirementDateAdjustedPayment(dates: RetirementDates, colaAdjustedPIA: number): number;
3
+ export declare function calcRetirement(birthday: Date, retirementDate: Date, earnings: Earnings): Partial<BenefitCalculationResult>;
4
4
  export declare function calculateRetirementDates(birthday: Date, retirementDate: Date): RetirementDates;
5
+ export declare function retirementDateAdjustedPayment(dates: RetirementDates, colaAdjustedPIA: number): number;
5
6
  export declare function calculatePIA(AIME: number, baseYear?: number): number;
7
+ export declare function calculateAIME(earnings: Earnings, lookbackYears: number, baseYear?: number): number;
6
8
  export declare function getLookbackYears(elapsedYears: number): number;
7
- export declare function calculateAIME(earnings: Earnings, yearStartCounter: number, effectiveDay: Date, baseYear?: number): number;
8
- export declare function getEnglishCommonLawDate(date: Date): Date;
9
+ export declare function getLookbackYearsDisability(elapsedYears: number): number;
9
10
  export declare function getFullRetirementMonths(commonLawBirthDate: Date): number;
11
+ /** Compute Family-Max bend points for an eligibility year given AWI_{year-2}. */
12
+ export declare function getFamilyMaxBendPoints(baseYear: number): [number, number, number];
13
+ /** Apply the family-maximum formula to a PIA using already-computed bend points. */
14
+ export declare function calculateFamilyMaximum(pia: number, [bp1, bp2, bp3]: [number, number, number]): number;
15
+ export declare function getEnglishCommonLawDate(date: Date): Date;
package/lib/index.js CHANGED
@@ -1,56 +1,84 @@
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 calculation function
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 } from './constants';
3
+ // Main entry point
4
4
  export function calc(birthday, retirementDate, earnings) {
5
- // Validation
6
- if (!birthday || !retirementDate) {
7
- throw new Error('Birthday and retirement date are required');
8
- }
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
+ const disabilityPIAFloored = context.retirementDate > context.dates.fullRetirement ? 0 : Math.floor(disabilityBenefit.colaAdjustedPIA);
17
+ return {
18
+ AIME: regularBenefit.aime,
19
+ PIA: regularBenefit.pia,
20
+ NormalMonthlyBenefit: monthlyBenefit,
21
+ DisabilityEarnings: disabilityPIAFloored,
22
+ };
23
+ }
24
+ // Reusable benefit calculation pipeline
25
+ function calculateBenefitPipeline(earnings, yearAge60, lookbackYears) {
26
+ const aime = calculateAIME(earnings, lookbackYears, yearAge60);
27
+ const pia = calculatePIA(aime, yearAge60);
28
+ const colaAdjustedPIA = calculateCOLAAdjustments(pia, yearAge60 + 2);
29
+ return { aime, pia, colaAdjustedPIA };
30
+ }
31
+ // Survivor benefits calculation
32
+ function calcSurvivor(birthday, retirementDate, earnings) {
33
+ const context = createCalculationContext(birthday, retirementDate, earnings);
34
+ const benefit = calculateBenefitPipeline(earnings, context.yearAge60, getLookbackYears(context.totalYears));
35
+ const survivorPIA = Math.floor(benefit.colaAdjustedPIA * CHILD_SURVIVOR_BENEFIT_PERCENTAGE);
36
+ const effectiveYear = Math.min(context.yearAge60, WAGE_INDEX_CUTOFF);
37
+ const wageIndexEntry = getWageIndexEntry(effectiveYear);
38
+ const wageIndexLastYear = wageIndexEntry.awi;
39
+ const familyMax = calculateFamilyMaximum(benefit.pia, getFamilyMaxBendPoints(wageIndexLastYear));
40
+ const colaAdjustedFamMax = calculateCOLAAdjustments(familyMax, context.yearAge60 + 2);
41
+ return {
42
+ survivingChild: survivorPIA,
43
+ careGivingSpouse: survivorPIA,
44
+ normalRetirementSpouse: Math.floor(benefit.colaAdjustedPIA),
45
+ familyMaximum: colaAdjustedFamMax
46
+ };
47
+ }
48
+ // Create shared calculation context to avoid duplication
49
+ function createCalculationContext(birthday, retirementDate, earnings) {
50
+ validateInputs(birthday, retirementDate, earnings);
9
51
  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
52
  const yearAge60 = birthday.getFullYear() + 60;
21
53
  const yearStartCounting = birthday.getFullYear() + ELAPSED_YEARS_START_AGE - 1;
22
- const averageIndexedMonthlyEarnings = calculateAIME(earnings, yearStartCounting, retirementDate, yearAge60);
23
- const age60Year = dates.eclBirthDate.getFullYear() + 60;
24
- const primaryInsuranceAmount = calculatePIA(averageIndexedMonthlyEarnings, age60Year);
25
- const survivorPIA = primaryInsuranceAmount * CHILD_SURVIVOR_BENEFIT_PERCENTAGE;
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);
54
+ const dateStartCounting = new Date(birthday);
55
+ dateStartCounting.setFullYear(yearStartCounting);
56
+ const monthsDiff = monthsDifference(retirementDate, dateStartCounting) / 12;
57
+ const totalYears = Math.min(LOOKBACK_YEARS, monthsDiff);
31
58
  return {
32
- AIME: averageIndexedMonthlyEarnings,
33
- PIA: primaryInsuranceAmount,
34
- NormalMonthlyBenefit: results,
35
- DisabilityEarnings: disabilityPIA,
59
+ birthday,
60
+ retirementDate,
61
+ earnings,
62
+ dates,
63
+ yearAge60,
64
+ yearStartCounting,
65
+ dateStartCounting,
66
+ totalYears
36
67
  };
37
68
  }
38
- export function retirementDateAdjustedPayment(dates, colaAdjustedPIA) {
39
- // Calculate early/delayed retirement adjustments
40
- const earlyRetireMonths = monthsDifference(dates.adjusted, dates.fullRetirement);
41
- let adjustedBenefits = colaAdjustedPIA;
42
- if (dates.retirementDate < dates.earliestRetirement) {
43
- adjustedBenefits = 0;
69
+ // Extracted validation logic
70
+ function validateInputs(birthday, retirementDate, earnings) {
71
+ if (!birthday || !retirementDate) {
72
+ throw new Error('Birthday and retirement date are required');
44
73
  }
45
- else if (earlyRetireMonths < 0) {
46
- adjustedBenefits = calculateEarlyRetirementReduction(colaAdjustedPIA, Math.abs(earlyRetireMonths));
74
+ if (!earnings || earnings.length === 0) {
75
+ throw new Error('Earnings history cannot be empty');
47
76
  }
48
- else if (earlyRetireMonths > 0) {
49
- adjustedBenefits = calculateDelayedRetirementIncrease(dates.eclBirthDate, colaAdjustedPIA, earlyRetireMonths);
77
+ if (retirementDate < birthday) {
78
+ throw new Error('Retirement date cannot be before birthday');
50
79
  }
51
- const monthlyBenefit = Math.floor(adjustedBenefits);
52
- return monthlyBenefit;
53
80
  }
81
+ // Retirement date calculations
54
82
  export function calculateRetirementDates(birthday, retirementDate) {
55
83
  const eclBirthDate = getEnglishCommonLawDate(birthday);
56
84
  const fraMonths = getFullRetirementMonths(eclBirthDate);
@@ -67,6 +95,22 @@ export function calculateRetirementDates(birthday, retirementDate) {
67
95
  retirementDate
68
96
  };
69
97
  }
98
+ // Benefit amount calculations
99
+ export function retirementDateAdjustedPayment(dates, colaAdjustedPIA) {
100
+ const earlyRetireMonths = monthsDifference(dates.adjusted, dates.fullRetirement);
101
+ let adjustedBenefits = colaAdjustedPIA;
102
+ if (dates.retirementDate < dates.earliestRetirement) {
103
+ adjustedBenefits = 0;
104
+ }
105
+ else if (earlyRetireMonths < 0) {
106
+ adjustedBenefits = calculateEarlyRetirementReduction(colaAdjustedPIA, Math.abs(earlyRetireMonths));
107
+ }
108
+ else if (earlyRetireMonths > 0) {
109
+ adjustedBenefits = calculateDelayedRetirementIncrease(dates.eclBirthDate, colaAdjustedPIA, earlyRetireMonths);
110
+ }
111
+ return Math.floor(adjustedBenefits);
112
+ }
113
+ // COLA adjustments
70
114
  function calculateCOLAAdjustments(PIA, startYear) {
71
115
  const currentYear = new Date().getFullYear();
72
116
  const colaRates = wageIndex
@@ -77,12 +121,10 @@ function calculateCOLAAdjustments(PIA, startYear) {
77
121
  return roundToFloorTenCents(adjustedAmount * multiplier);
78
122
  }, PIA);
79
123
  }
124
+ // PIA calculation
80
125
  export function calculatePIA(AIME, baseYear) {
81
126
  const effectiveYear = baseYear ? Math.min(baseYear, WAGE_INDEX_CUTOFF) : WAGE_INDEX_CUTOFF;
82
- const wageIndexEntry = wageIndex.find(val => val.year === effectiveYear);
83
- if (!wageIndexEntry) {
84
- throw new Error(`No wage index data found for year ${effectiveYear}`);
85
- }
127
+ const wageIndexEntry = getWageIndexEntry(effectiveYear);
86
128
  const wageIndexLastYear = wageIndexEntry.awi;
87
129
  // Calculate bend points (rounded to nearest dollar per SSA rules)
88
130
  const firstBendPoint = Math.round(FIRST_BEND_POINT_MULTIPLIER * wageIndexLastYear / BEND_POINT_DIVISOR);
@@ -104,23 +146,13 @@ export function calculatePIA(AIME, baseYear) {
104
146
  }
105
147
  return roundToFloorTenCents(monthlyBenefit);
106
148
  }
107
- export function getLookbackYears(elapsedYears) {
108
- const minComputationYears = 2;
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);
149
+ // AIME calculation
150
+ export function calculateAIME(earnings, lookbackYears, baseYear) {
116
151
  if (!earnings || earnings.length === 0) {
117
152
  return 0;
118
153
  }
119
154
  const effectiveYear = baseYear ? Math.min(baseYear, WAGE_INDEX_CUTOFF) : WAGE_INDEX_CUTOFF;
120
- const wageIndexEntry = wageIndex.find(val => val.year === effectiveYear);
121
- if (!wageIndexEntry) {
122
- throw new Error(`No wage index data found for year ${effectiveYear}`);
123
- }
155
+ const wageIndexEntry = getWageIndexEntry(effectiveYear);
124
156
  const wageIndexLastYear = wageIndexEntry.awi;
125
157
  const futureYearsFactor = 1;
126
158
  // Calculate wage index factors
@@ -133,15 +165,27 @@ export function calculateAIME(earnings, yearStartCounter, effectiveDay, baseYear
133
165
  acc[year] = earnings * (wageIndexFactors[year] || futureYearsFactor);
134
166
  return acc;
135
167
  }, {});
136
- // Get top 35 years of earnings
137
- const top35YearsEarningsArr = Object.values(adjustedEarnings)
168
+ // Get top years of earnings based on lookback period
169
+ const topYearsEarningsArr = Object.values(adjustedEarnings)
138
170
  .sort((a, b) => b - a)
139
171
  .slice(0, lookbackYears);
140
- const totalEarnings = top35YearsEarningsArr.reduce((sum, earnings) => sum + earnings, 0);
172
+ const totalEarnings = topYearsEarningsArr.reduce((sum, earnings) => sum + earnings, 0);
141
173
  // Calculate AIME (rounded down to next lower dollar)
142
- const averageIndexedMonthlyEarnings = Math.floor(totalEarnings / (12 * lookbackYears));
143
- return averageIndexedMonthlyEarnings;
174
+ return Math.floor(totalEarnings / (12 * lookbackYears));
175
+ }
176
+ // Lookback year calculations
177
+ export function getLookbackYears(elapsedYears) {
178
+ const minComputationYears = 2;
179
+ const adjustedLookbackYears = Math.floor(elapsedYears) - 5;
180
+ return Math.max(minComputationYears, adjustedLookbackYears);
181
+ }
182
+ export function getLookbackYearsDisability(elapsedYears) {
183
+ const minComputationYears = 2;
184
+ const dropOutYears = Math.min(Math.floor(elapsedYears / DROP_OUT_YEARS_DIVISOR), MAX_DROP_OUT_YEARS);
185
+ const adjustedLookbackYears = elapsedYears - dropOutYears;
186
+ return Math.max(minComputationYears, adjustedLookbackYears);
144
187
  }
188
+ // Early retirement reduction
145
189
  function calculateEarlyRetirementReduction(amount, months) {
146
190
  if (months <= 0)
147
191
  return amount;
@@ -156,6 +200,7 @@ function calculateEarlyRetirementReduction(amount, months) {
156
200
  }
157
201
  return amount * (1 - reduction);
158
202
  }
203
+ // Delayed retirement increase
159
204
  function calculateDelayedRetirementIncrease(birthday, initialAmount, numberOfMonths) {
160
205
  if (numberOfMonths <= 0)
161
206
  return initialAmount;
@@ -164,11 +209,11 @@ function calculateDelayedRetirementIncrease(birthday, initialAmount, numberOfMon
164
209
  const totalIncrease = monthlyRate * numberOfMonths;
165
210
  return initialAmount * (1 + totalIncrease);
166
211
  }
212
+ // Delayed retirement rate lookup
167
213
  function getDelayedRetirementRate(birthYear) {
168
214
  if (birthYear < 1933) {
169
215
  throw new Error(`Invalid birth year for delayed retirement: ${birthYear}`);
170
216
  }
171
- // Rates based on SSA rules
172
217
  if (birthYear <= 1934)
173
218
  return 11 / 24 / 100; // 11/24 of 1%
174
219
  if (birthYear <= 1936)
@@ -181,30 +226,19 @@ function getDelayedRetirementRate(birthYear) {
181
226
  return 5 / 8 / 100; // 5/8 of 1%
182
227
  return 2 / 3 / 100; // 2/3 of 1%
183
228
  }
184
- function roundToFloorTenCents(amount) {
185
- // Convert to dimes, floor, then convert back to dollars
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
- }
229
+ // Full retirement age calculation
194
230
  export function getFullRetirementMonths(commonLawBirthDate) {
195
231
  const year = commonLawBirthDate.getFullYear();
196
232
  if (year <= 1937) {
197
233
  return 65 * 12;
198
234
  }
199
235
  else if (year <= 1942) {
200
- // Gradual increase from 65 years to 65 years 10 months
201
236
  return ((year - 1937) * 2) + (65 * 12);
202
237
  }
203
238
  else if (year <= 1954) {
204
239
  return 66 * 12;
205
240
  }
206
241
  else if (year <= 1959) {
207
- // Gradual increase from 66 years to 66 years 10 months
208
242
  return ((year - 1954) * 2) + (66 * 12);
209
243
  }
210
244
  else if (year >= 1960) {
@@ -214,8 +248,42 @@ export function getFullRetirementMonths(commonLawBirthDate) {
214
248
  throw new Error(`Invalid birth year: ${year}`);
215
249
  }
216
250
  }
251
+ /** Compute Family-Max bend points for an eligibility year given AWI_{year-2}. */
252
+ export function getFamilyMaxBendPoints(baseYear) {
253
+ const [f1, f2, f3] = FAM_MAX_BASES.map(b => Math.round((b * baseYear / BEND_POINT_DIVISOR)));
254
+ return [f1, f2, f3];
255
+ }
256
+ /** Apply the family-maximum formula to a PIA using already-computed bend points. */
257
+ export function calculateFamilyMaximum(pia, [bp1, bp2, bp3]) {
258
+ let max = 0;
259
+ max += 1.50 * Math.min(pia, bp1);
260
+ if (pia > bp1)
261
+ max += 2.72 * (Math.min(pia, bp2) - bp1);
262
+ if (pia > bp2)
263
+ max += 1.34 * (Math.min(pia, bp3) - bp2);
264
+ if (pia > bp3)
265
+ max += 1.75 * (pia - bp3);
266
+ // SSA: round total down to next lower $0.10
267
+ return Math.floor(max * 10) / 10;
268
+ }
269
+ // Utility functions
270
+ export function getEnglishCommonLawDate(date) {
271
+ const eclDate = new Date(date);
272
+ eclDate.setDate(eclDate.getDate() - 1);
273
+ return eclDate;
274
+ }
217
275
  function monthsDifference(date1, date2) {
218
276
  const months1 = date1.getFullYear() * 12 + date1.getMonth();
219
277
  const months2 = date2.getFullYear() * 12 + date2.getMonth();
220
278
  return months1 - months2;
221
279
  }
280
+ function roundToFloorTenCents(amount) {
281
+ return Math.floor(amount * 10) / 10;
282
+ }
283
+ function getWageIndexEntry(effectiveYear) {
284
+ const wageIndexEntry = wageIndex.find(val => val.year === effectiveYear);
285
+ if (!wageIndexEntry) {
286
+ throw new Error(`No wage index data found for year ${effectiveYear}`);
287
+ }
288
+ return wageIndexEntry;
289
+ }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-security-calculator",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Calculate estimated Social Security Benefits",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "minimist": "^1.2.8",
48
48
  "playwright": "^1.54.1",
49
49
  "ts-jest": "^29.1.1",
50
- "typescript": "^4.8.3"
50
+ "typescript": "^5.9.2"
51
51
  },
52
52
  "dependencies": {
53
53
  "xml2js": "^0.6.2"