social-security-calculator 1.0.1 → 1.0.3
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/README.md +133 -8
- package/lib/constants.d.ts +17 -0
- package/lib/constants.js +18 -0
- package/lib/estimatedEarnings/index.d.ts +2 -2
- package/lib/estimatedEarnings/index.js +6 -5
- package/lib/index.d.ts +5 -10
- package/lib/index.js +157 -128
- package/lib/model.d.ts +21 -1
- package/lib/parseStatement/index.d.ts +2 -2
- package/lib/wage-index.d.ts +2 -3
- package/lib/wage-index.js +701 -155
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,139 @@
|
|
|
1
|
-
#
|
|
2
|
-
TypeScript to calculate estimated Social Security Benefits
|
|
1
|
+
# Social Security Benefits Calculator
|
|
3
2
|
|
|
3
|
+
A TypeScript library for calculating Social Security retirement benefits based on official SSA formulas. This calculator handles Average Indexed Monthly Earnings (AIME), Primary Insurance Amount (PIA), and adjustments for early or delayed retirement.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from Social Security given your annual earnings
|
|
5
|
+
## Credits
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
This project is forked from [Ryan Antkowiak's SocialSecurityCalculator](https://github.com/antkowiak/SocialSecurityCalculator) python implementation. The original implementation has been refactored with COLA adjustments to bring it into alignment with SSA's Quick Calculator.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
Security eligible earnings in that particular year
|
|
9
|
+
## Features
|
|
12
10
|
|
|
11
|
+
- Calculate Average Indexed Monthly Earnings (AIME) based on earnings history
|
|
12
|
+
- Determine Primary Insurance Amount (PIA) using current bend points
|
|
13
|
+
- Apply Cost of Living Adjustments (COLA)
|
|
14
|
+
- Handle early retirement reductions
|
|
15
|
+
- Calculate delayed retirement credits
|
|
16
|
+
- Full TypeScript support with comprehensive type definitions
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install social-security-calculator
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { calc } from 'social-security-calculator';
|
|
30
|
+
|
|
31
|
+
const result = calc(
|
|
32
|
+
new Date(1960, 0, 15), // Birthday: January 15, 1960
|
|
33
|
+
new Date(2027, 0, 15), // Retirement: January 15, 2027
|
|
34
|
+
[
|
|
35
|
+
{ year: 1982, earnings: 12000 },
|
|
36
|
+
{ year: 1983, earnings: 15000 },
|
|
37
|
+
// ... add more years of earnings
|
|
38
|
+
{ year: 2025, earnings: 87000 }
|
|
39
|
+
]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
console.log(`Monthly benefit: ${result.NormalMonthlyBenefit}`);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Early Retirement Example
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const birthDate = new Date(1965, 5, 1); // June 1, 1965
|
|
49
|
+
const earlyRetirement = new Date(2027, 5, 1); // June 1, 2027 (at age 62)
|
|
50
|
+
|
|
51
|
+
const result = calc(birthDate, earlyRetirement, earnings);
|
|
52
|
+
// Benefits will be reduced for early retirement
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Delayed Retirement Example
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const birthDate = new Date(1955, 2, 15); // March 15, 1955
|
|
59
|
+
const delayedRetirement = new Date(2025, 2, 15); // March 15, 2025 (at age 70)
|
|
60
|
+
|
|
61
|
+
const result = calc(birthDate, delayedRetirement, earnings);
|
|
62
|
+
// Benefits will include delayed retirement credits
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### Main Functions
|
|
68
|
+
|
|
69
|
+
#### `calc(birthday: Date, retirementDate: Date, earnings: Earnings): BenefitCalculationResult`
|
|
70
|
+
|
|
71
|
+
Calculates Social Security retirement benefits.
|
|
72
|
+
|
|
73
|
+
**Parameters:**
|
|
74
|
+
- `birthday`: Date of birth
|
|
75
|
+
- `retirementDate`: Planned retirement date
|
|
76
|
+
- `earnings`: Array of yearly earnings records
|
|
77
|
+
|
|
78
|
+
**Returns:**
|
|
79
|
+
- `BenefitCalculationResult` object containing:
|
|
80
|
+
- `AIME`: Average Indexed Monthly Earnings
|
|
81
|
+
- `NormalMonthlyBenefit`: Monthly benefit amount (in dollars)
|
|
82
|
+
|
|
83
|
+
#### `calculatePIA(AIME: number, baseYear?: number): number`
|
|
84
|
+
|
|
85
|
+
Calculates the Primary Insurance Amount based on AIME.
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
- `AIME`: Average Indexed Monthly Earnings
|
|
89
|
+
- `baseYear`: Optional base year for calculations (defaults to 2023)
|
|
90
|
+
|
|
91
|
+
#### `calculateAIME(earnings: Earnings, baseYear?: number): number`
|
|
92
|
+
|
|
93
|
+
Calculates Average Indexed Monthly Earnings from earnings history.
|
|
94
|
+
|
|
95
|
+
**Parameters:**
|
|
96
|
+
- `earnings`: Array of yearly earnings
|
|
97
|
+
- `baseYear`: Optional base year for wage indexing
|
|
98
|
+
|
|
99
|
+
### Type Definitions
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface Wage {
|
|
103
|
+
year: number;
|
|
104
|
+
earnings: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type Earnings = Wage[];
|
|
108
|
+
|
|
109
|
+
interface BenefitCalculationResult {
|
|
110
|
+
AIME: number;
|
|
111
|
+
NormalMonthlyBenefit: number;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## How It Works
|
|
116
|
+
|
|
117
|
+
1. **AIME Calculation**: The calculator indexes historical earnings to account for wage inflation, selects the highest 35 years of indexed earnings, and computes the monthly average.
|
|
118
|
+
|
|
119
|
+
2. **PIA Calculation**: Applies SSA bend points to determine the Primary Insurance Amount from the AIME.
|
|
120
|
+
|
|
121
|
+
3. **Retirement Age Adjustments**:
|
|
122
|
+
- Early retirement (age 62-66): Benefits are reduced by 5/9 of 1% for each of the first 36 months and 5/12 of 1% for additional months
|
|
123
|
+
- Delayed retirement (age 67-70): Benefits increase by 2/3 of 1% per month (8% per year) for those born after 1943
|
|
124
|
+
|
|
125
|
+
4. **COLA Adjustments**: Annual Cost of Living Adjustments are applied starting at age 62.
|
|
126
|
+
|
|
127
|
+
## Important Notes
|
|
128
|
+
|
|
129
|
+
- The calculator uses the most recent wage indexes as published by the SSA as of 2025
|
|
130
|
+
- It is designed and tested to exactly align with the [SSA Quick Calculator](https://www.ssa.gov/OACT/quickcalc/index.html)
|
|
131
|
+
- It always uses current dollar value, and does not predict future inflation amounts
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
This project maintains the same license as the original repository. Please refer to the LICENSE file for details.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const EARLY_RETIREMENT_AGE = 62;
|
|
2
|
+
export declare const WAGE_INDEX_CUTOFF = 2023;
|
|
3
|
+
export declare const MAX_RETIREMENT_AGE = 70;
|
|
4
|
+
export declare const LOOKBACK_YEARS = 35;
|
|
5
|
+
export declare const BEND_POINT_DIVISOR = 9779.44;
|
|
6
|
+
export declare const FIRST_BEND_POINT_MULTIPLIER = 180;
|
|
7
|
+
export declare const SECOND_BEND_POINT_MULTIPLIER = 1085;
|
|
8
|
+
export declare const PIA_PERCENTAGES: {
|
|
9
|
+
readonly FIRST_BRACKET: 0.9;
|
|
10
|
+
readonly SECOND_BRACKET: 0.32;
|
|
11
|
+
readonly THIRD_BRACKET: 0.15;
|
|
12
|
+
};
|
|
13
|
+
export declare const EARLY_RETIREMENT_REDUCTION: {
|
|
14
|
+
readonly FIRST_MONTHS: 36;
|
|
15
|
+
readonly FIRST_MONTHS_RATE: number;
|
|
16
|
+
readonly ADDITIONAL_MONTHS_RATE: number;
|
|
17
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Constants
|
|
2
|
+
export const EARLY_RETIREMENT_AGE = 62;
|
|
3
|
+
export const WAGE_INDEX_CUTOFF = 2023;
|
|
4
|
+
export const MAX_RETIREMENT_AGE = 70;
|
|
5
|
+
export const LOOKBACK_YEARS = 35;
|
|
6
|
+
export const BEND_POINT_DIVISOR = 9779.44;
|
|
7
|
+
export const FIRST_BEND_POINT_MULTIPLIER = 180;
|
|
8
|
+
export const SECOND_BEND_POINT_MULTIPLIER = 1085;
|
|
9
|
+
export const PIA_PERCENTAGES = {
|
|
10
|
+
FIRST_BRACKET: 0.9,
|
|
11
|
+
SECOND_BRACKET: 0.32,
|
|
12
|
+
THIRD_BRACKET: 0.15
|
|
13
|
+
};
|
|
14
|
+
export const EARLY_RETIREMENT_REDUCTION = {
|
|
15
|
+
FIRST_MONTHS: 36,
|
|
16
|
+
FIRST_MONTHS_RATE: 5 / 9 * 0.01,
|
|
17
|
+
ADDITIONAL_MONTHS_RATE: 5 / 12 * 0.01 // 5/12 of 1%
|
|
18
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function getEstimatedEarnings(birthDate: Date, lastWage: number, lastYearWorked?: number, earningGrowthRate?: number):
|
|
1
|
+
import { Earnings } from '../model';
|
|
2
|
+
export declare function getEstimatedEarnings(birthDate: Date, lastWage: number, lastYearWorked?: number, earningGrowthRate?: number): Earnings;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { wageIndex
|
|
1
|
+
import { wageIndex } from '../wage-index';
|
|
2
|
+
import { getEnglishCommonLawDate } from '../index';
|
|
2
3
|
const YOUTH_FACTOR = 8;
|
|
3
4
|
const YOUTH_FACTOR_AGE = 21;
|
|
4
5
|
const WORK_START_AGE = 18;
|
|
5
6
|
const CURRENT_YEAR = new Date().getFullYear();
|
|
6
7
|
export function getEstimatedEarnings(birthDate, lastWage, lastYearWorked = CURRENT_YEAR, earningGrowthRate = 0) {
|
|
7
8
|
var _a;
|
|
8
|
-
const birthDateMinusOneDay =
|
|
9
|
+
const birthDateMinusOneDay = getEnglishCommonLawDate(birthDate);
|
|
9
10
|
const age = CURRENT_YEAR - birthDateMinusOneDay.getFullYear();
|
|
10
11
|
if (age <= 22) {
|
|
11
12
|
// return zero
|
|
@@ -30,7 +31,7 @@ export function getEstimatedEarnings(birthDate, lastWage, lastYearWorked = CURRE
|
|
|
30
31
|
}
|
|
31
32
|
// Cap wages at taxable maximum
|
|
32
33
|
wageResults.forEach(({ year, earnings }) => {
|
|
33
|
-
const maxTaxable =
|
|
34
|
+
const maxTaxable = wageIndex.find((entry) => entry.year === year).taxMax;
|
|
34
35
|
const currentEarnings = wageResults.find((entry) => entry.year === year).earnings;
|
|
35
36
|
const maxEarnings = Math.min(currentEarnings, maxTaxable);
|
|
36
37
|
wageResults.find((entry) => entry.year === year).earnings = maxEarnings;
|
|
@@ -38,7 +39,7 @@ export function getEstimatedEarnings(birthDate, lastWage, lastYearWorked = CURRE
|
|
|
38
39
|
return wageResults;
|
|
39
40
|
}
|
|
40
41
|
function getReductionFactor(year) {
|
|
41
|
-
const allIndexes = wageIndex
|
|
42
|
+
const allIndexes = wageIndex;
|
|
42
43
|
const lastYear = year - 1;
|
|
43
44
|
const nextYear = year + 1;
|
|
44
45
|
if (year === CURRENT_YEAR && !allIndexes.find((entry) => entry.year === lastYear)) {
|
|
@@ -48,6 +49,6 @@ function getReductionFactor(year) {
|
|
|
48
49
|
return 1;
|
|
49
50
|
}
|
|
50
51
|
else {
|
|
51
|
-
return ((allIndexes.find((entry) => entry.year === year).
|
|
52
|
+
return ((allIndexes.find((entry) => entry.year === year).awi) / (allIndexes.find((entry) => entry.year === nextYear).awi));
|
|
52
53
|
}
|
|
53
54
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function calc(birthday: Date, retirementDate: Date, earnings:
|
|
3
|
-
|
|
4
|
-
NormalMonthlyBenefit: number;
|
|
5
|
-
};
|
|
6
|
-
export declare function calcRetirementBenefit(birthday: Date, retirementDate: Date, AIME: number): {
|
|
7
|
-
AIME: number;
|
|
8
|
-
NormalMonthlyBenefit: number;
|
|
9
|
-
};
|
|
1
|
+
import { BenefitCalculationResult, Earnings } from './model';
|
|
2
|
+
export declare function calc(birthday: Date, retirementDate: Date, earnings: Earnings): BenefitCalculationResult;
|
|
3
|
+
export declare function calcRetirementBenefit(birthday: Date, retirementDate: Date, AIME: number): BenefitCalculationResult;
|
|
10
4
|
export declare function calculatePIA(AIME: number, baseYear?: number): number;
|
|
11
|
-
export declare function calculateAIME(earnings:
|
|
5
|
+
export declare function calculateAIME(earnings: Earnings, baseYear?: number): number;
|
|
6
|
+
export declare function getEnglishCommonLawDate(date: Date): Date;
|
|
12
7
|
export declare function getFullRetirementMonths(commonLawBirthDate: Date): number;
|
package/lib/index.js
CHANGED
|
@@ -1,173 +1,202 @@
|
|
|
1
1
|
import { wageIndex } from './wage-index';
|
|
2
|
-
|
|
2
|
+
import { EARLY_RETIREMENT_AGE, WAGE_INDEX_CUTOFF, MAX_RETIREMENT_AGE, LOOKBACK_YEARS, BEND_POINT_DIVISOR, FIRST_BEND_POINT_MULTIPLIER, SECOND_BEND_POINT_MULTIPLIER, PIA_PERCENTAGES, EARLY_RETIREMENT_REDUCTION } from './constants';
|
|
3
|
+
// Main calculation function
|
|
3
4
|
export function calc(birthday, retirementDate, earnings) {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
// Validation
|
|
6
|
+
if (!birthday || !retirementDate) {
|
|
7
|
+
throw new Error('Birthday and retirement date are required');
|
|
8
|
+
}
|
|
9
|
+
if (!earnings || earnings.length === 0) {
|
|
10
|
+
throw new Error('Earnings history cannot be empty');
|
|
11
|
+
}
|
|
12
|
+
if (retirementDate < birthday) {
|
|
13
|
+
throw new Error('Retirement date cannot be before birthday');
|
|
14
|
+
}
|
|
15
|
+
const yearAge60 = birthday.getFullYear() + 60;
|
|
16
|
+
const averageIndexedMonthlyEarnings = calculateAIME(earnings, yearAge60);
|
|
17
|
+
const results = calcRetirementBenefit(birthday, retirementDate, averageIndexedMonthlyEarnings);
|
|
6
18
|
return results;
|
|
7
19
|
}
|
|
8
20
|
export function calcRetirementBenefit(birthday, retirementDate, AIME) {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const earlyRetireMonths = monthsDifference(
|
|
16
|
-
let adjustedBenefits =
|
|
17
|
-
if (retirementDate <
|
|
21
|
+
const dates = calculateRetirementDates(birthday, retirementDate);
|
|
22
|
+
const age60Year = dates.eclBirthDate.getFullYear() + 60;
|
|
23
|
+
const primaryInsuranceAmount = calculatePIA(AIME, age60Year);
|
|
24
|
+
// Calculate COLA adjustments
|
|
25
|
+
const colaAdjustedPIA = calculateCOLAAdjustments(primaryInsuranceAmount, age60Year + 2);
|
|
26
|
+
// Calculate early/delayed retirement adjustments
|
|
27
|
+
const earlyRetireMonths = monthsDifference(dates.adjusted, dates.fullRetirement);
|
|
28
|
+
let adjustedBenefits = colaAdjustedPIA;
|
|
29
|
+
if (retirementDate < dates.earliestRetirement) {
|
|
18
30
|
adjustedBenefits = 0;
|
|
19
31
|
}
|
|
20
32
|
else if (earlyRetireMonths < 0) {
|
|
21
|
-
adjustedBenefits =
|
|
33
|
+
adjustedBenefits = calculateEarlyRetirementReduction(colaAdjustedPIA, Math.abs(earlyRetireMonths));
|
|
22
34
|
}
|
|
23
35
|
else if (earlyRetireMonths > 0) {
|
|
24
|
-
adjustedBenefits =
|
|
36
|
+
adjustedBenefits = calculateDelayedRetirementIncrease(dates.eclBirthDate, colaAdjustedPIA, earlyRetireMonths);
|
|
25
37
|
}
|
|
26
38
|
const monthlyBenefit = Math.floor(adjustedBenefits);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
return {
|
|
40
|
+
AIME: AIME,
|
|
41
|
+
NormalMonthlyBenefit: monthlyBenefit,
|
|
30
42
|
};
|
|
31
|
-
|
|
43
|
+
}
|
|
44
|
+
function calculateRetirementDates(birthday, retirementDate) {
|
|
45
|
+
const eclBirthDate = getEnglishCommonLawDate(birthday);
|
|
46
|
+
const fraMonths = getFullRetirementMonths(eclBirthDate);
|
|
47
|
+
const earliestRetirement = new Date(eclBirthDate.getFullYear() + EARLY_RETIREMENT_AGE, eclBirthDate.getMonth(), eclBirthDate.getDate());
|
|
48
|
+
const fullRetirement = new Date(eclBirthDate.getFullYear(), eclBirthDate.getMonth() + fraMonths, eclBirthDate.getDate());
|
|
49
|
+
const maxRetirement = new Date(eclBirthDate.getFullYear() + MAX_RETIREMENT_AGE, eclBirthDate.getMonth(), eclBirthDate.getDate());
|
|
50
|
+
const adjusted = retirementDate > maxRetirement ? maxRetirement : retirementDate;
|
|
51
|
+
return {
|
|
52
|
+
earliestRetirement,
|
|
53
|
+
fullRetirement,
|
|
54
|
+
maxRetirement,
|
|
55
|
+
adjusted,
|
|
56
|
+
eclBirthDate
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function calculateCOLAAdjustments(PIA, startYear) {
|
|
60
|
+
const currentYear = new Date().getFullYear();
|
|
61
|
+
const colaRates = wageIndex
|
|
62
|
+
.filter(wage => wage.year >= startYear && wage.year < currentYear)
|
|
63
|
+
.map(wage => wage.cola);
|
|
64
|
+
return colaRates.reduce((adjustedAmount, rate) => {
|
|
65
|
+
const multiplier = 1 + (rate / 100);
|
|
66
|
+
return roundToFloorTenCents(adjustedAmount * multiplier);
|
|
67
|
+
}, PIA);
|
|
32
68
|
}
|
|
33
69
|
export function calculatePIA(AIME, baseYear) {
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
else {
|
|
60
|
-
monthlyBenefit = (0.9 * firstBendPoint) + (0.32 * (secondBendPoint - firstBendPoint)) + (0.15 * (AIME - secondBendPoint));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return roundToFloorTenCents(monthlyBenefit);
|
|
64
|
-
})();
|
|
65
|
-
return PIA;
|
|
70
|
+
const effectiveYear = baseYear ? Math.min(baseYear, WAGE_INDEX_CUTOFF) : WAGE_INDEX_CUTOFF;
|
|
71
|
+
const wageIndexEntry = wageIndex.find(val => val.year === effectiveYear);
|
|
72
|
+
if (!wageIndexEntry) {
|
|
73
|
+
throw new Error(`No wage index data found for year ${effectiveYear}`);
|
|
74
|
+
}
|
|
75
|
+
const wageIndexLastYear = wageIndexEntry.awi;
|
|
76
|
+
// Calculate bend points (rounded to nearest dollar per SSA rules)
|
|
77
|
+
const firstBendPoint = Math.round(FIRST_BEND_POINT_MULTIPLIER * wageIndexLastYear / BEND_POINT_DIVISOR);
|
|
78
|
+
const secondBendPoint = Math.round(SECOND_BEND_POINT_MULTIPLIER * wageIndexLastYear / BEND_POINT_DIVISOR);
|
|
79
|
+
let monthlyBenefit;
|
|
80
|
+
if (AIME <= firstBendPoint) {
|
|
81
|
+
monthlyBenefit = PIA_PERCENTAGES.FIRST_BRACKET * AIME;
|
|
82
|
+
}
|
|
83
|
+
else if (AIME <= secondBendPoint) {
|
|
84
|
+
monthlyBenefit =
|
|
85
|
+
PIA_PERCENTAGES.FIRST_BRACKET * firstBendPoint +
|
|
86
|
+
PIA_PERCENTAGES.SECOND_BRACKET * (AIME - firstBendPoint);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
monthlyBenefit =
|
|
90
|
+
PIA_PERCENTAGES.FIRST_BRACKET * firstBendPoint +
|
|
91
|
+
PIA_PERCENTAGES.SECOND_BRACKET * (secondBendPoint - firstBendPoint) +
|
|
92
|
+
PIA_PERCENTAGES.THIRD_BRACKET * (AIME - secondBendPoint);
|
|
93
|
+
}
|
|
94
|
+
return roundToFloorTenCents(monthlyBenefit);
|
|
66
95
|
}
|
|
67
96
|
export function calculateAIME(earnings, baseYear) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
if (!earnings || earnings.length === 0) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const effectiveYear = baseYear ? Math.min(baseYear, WAGE_INDEX_CUTOFF) : WAGE_INDEX_CUTOFF;
|
|
101
|
+
const wageIndexEntry = wageIndex.find(val => val.year === effectiveYear);
|
|
102
|
+
if (!wageIndexEntry) {
|
|
103
|
+
throw new Error(`No wage index data found for year ${effectiveYear}`);
|
|
104
|
+
}
|
|
105
|
+
const wageIndexLastYear = wageIndexEntry.awi;
|
|
76
106
|
const futureYearsFactor = 1;
|
|
77
|
-
//
|
|
78
|
-
const wageIndexFactors = wageIndex.reduce((acc, { year,
|
|
79
|
-
|
|
107
|
+
// Calculate wage index factors
|
|
108
|
+
const wageIndexFactors = wageIndex.reduce((acc, { year, awi }) => {
|
|
109
|
+
acc[year] = 1 + (Math.max(0, wageIndexLastYear - awi)) / awi;
|
|
110
|
+
return acc;
|
|
111
|
+
}, {});
|
|
112
|
+
// Adjust earnings according to wage index factor
|
|
80
113
|
const adjustedEarnings = earnings.reduce((acc, { year, earnings }) => {
|
|
81
114
|
acc[year] = earnings * (wageIndexFactors[year] || futureYearsFactor);
|
|
82
115
|
return acc;
|
|
83
116
|
}, {});
|
|
117
|
+
// Get top 35 years of earnings
|
|
84
118
|
const top35YearsEarningsArr = Object.values(adjustedEarnings)
|
|
85
|
-
.sort((a, b) => b - a)
|
|
86
|
-
.slice(0,
|
|
87
|
-
const
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return AIME;
|
|
119
|
+
.sort((a, b) => b - a)
|
|
120
|
+
.slice(0, LOOKBACK_YEARS);
|
|
121
|
+
const totalEarnings = top35YearsEarningsArr.reduce((sum, earnings) => sum + earnings, 0);
|
|
122
|
+
// Calculate AIME (rounded down to next lower dollar)
|
|
123
|
+
const averageIndexedMonthlyEarnings = Math.floor(totalEarnings / (12 * LOOKBACK_YEARS));
|
|
124
|
+
return averageIndexedMonthlyEarnings;
|
|
92
125
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let reduction
|
|
97
|
-
if (months <=
|
|
98
|
-
reduction = months *
|
|
126
|
+
function calculateEarlyRetirementReduction(amount, months) {
|
|
127
|
+
if (months <= 0)
|
|
128
|
+
return amount;
|
|
129
|
+
let reduction;
|
|
130
|
+
if (months <= EARLY_RETIREMENT_REDUCTION.FIRST_MONTHS) {
|
|
131
|
+
reduction = months * EARLY_RETIREMENT_REDUCTION.FIRST_MONTHS_RATE;
|
|
99
132
|
}
|
|
100
133
|
else {
|
|
101
|
-
|
|
102
|
-
|
|
134
|
+
reduction =
|
|
135
|
+
EARLY_RETIREMENT_REDUCTION.FIRST_MONTHS * EARLY_RETIREMENT_REDUCTION.FIRST_MONTHS_RATE +
|
|
136
|
+
(months - EARLY_RETIREMENT_REDUCTION.FIRST_MONTHS) * EARLY_RETIREMENT_REDUCTION.ADDITIONAL_MONTHS_RATE;
|
|
103
137
|
}
|
|
104
|
-
return amount
|
|
138
|
+
return amount * (1 - reduction);
|
|
105
139
|
}
|
|
106
|
-
function
|
|
140
|
+
function calculateDelayedRetirementIncrease(birthday, initialAmount, numberOfMonths) {
|
|
141
|
+
if (numberOfMonths <= 0)
|
|
142
|
+
return initialAmount;
|
|
107
143
|
const birthYear = birthday.getFullYear();
|
|
108
|
-
|
|
109
|
-
|
|
144
|
+
const monthlyRate = getDelayedRetirementRate(birthYear);
|
|
145
|
+
const totalIncrease = monthlyRate * numberOfMonths;
|
|
146
|
+
return initialAmount * (1 + totalIncrease);
|
|
147
|
+
}
|
|
148
|
+
function getDelayedRetirementRate(birthYear) {
|
|
110
149
|
if (birthYear < 1933) {
|
|
111
150
|
throw new Error(`Invalid birth year for delayed retirement: ${birthYear}`);
|
|
112
151
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
else if (birthYear <= 1942) {
|
|
126
|
-
monthlyRate = 5 / 8 / 100; // 5/8 of 1%
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
monthlyRate = 2 / 3 / 100; // 2/3 of 1%
|
|
130
|
-
}
|
|
131
|
-
// Calculate the new amount
|
|
132
|
-
let totalAdjustment = 0;
|
|
133
|
-
for (let i = 0; i < numberOfMonths; i++) {
|
|
134
|
-
totalAdjustment += monthlyRate;
|
|
135
|
-
}
|
|
136
|
-
return initialAmount * (1 + totalAdjustment);
|
|
152
|
+
// Rates based on SSA rules
|
|
153
|
+
if (birthYear <= 1934)
|
|
154
|
+
return 11 / 24 / 100; // 11/24 of 1%
|
|
155
|
+
if (birthYear <= 1936)
|
|
156
|
+
return 0.005; // 1/2 of 1%
|
|
157
|
+
if (birthYear <= 1938)
|
|
158
|
+
return 13 / 24 / 100; // 13/24 of 1%
|
|
159
|
+
if (birthYear <= 1940)
|
|
160
|
+
return 7 / 12 / 100; // 7/12 of 1%
|
|
161
|
+
if (birthYear <= 1942)
|
|
162
|
+
return 5 / 8 / 100; // 5/8 of 1%
|
|
163
|
+
return 2 / 3 / 100; // 2/3 of 1%
|
|
137
164
|
}
|
|
138
165
|
function roundToFloorTenCents(amount) {
|
|
139
|
-
// Convert
|
|
140
|
-
|
|
141
|
-
// floor to only whole dimes
|
|
142
|
-
dimes = Math.floor(dimes);
|
|
143
|
-
// Convert back to dollars and return
|
|
144
|
-
return (dimes / 10);
|
|
166
|
+
// Convert to dimes, floor, then convert back to dollars
|
|
167
|
+
return Math.floor(amount * 10) / 10;
|
|
145
168
|
}
|
|
146
|
-
function getEnglishCommonLawDate(date) {
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return englishCommonLawDate;
|
|
169
|
+
export function getEnglishCommonLawDate(date) {
|
|
170
|
+
// Create a new date to avoid mutating the original
|
|
171
|
+
const eclDate = new Date(date);
|
|
172
|
+
eclDate.setDate(eclDate.getDate() - 1);
|
|
173
|
+
return eclDate;
|
|
152
174
|
}
|
|
153
175
|
export function getFullRetirementMonths(commonLawBirthDate) {
|
|
154
176
|
const year = commonLawBirthDate.getFullYear();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
if (year <= 1937) {
|
|
178
|
+
return 65 * 12;
|
|
179
|
+
}
|
|
180
|
+
else if (year <= 1942) {
|
|
181
|
+
// Gradual increase from 65 years to 65 years 10 months
|
|
182
|
+
return ((year - 1937) * 2) + (65 * 12);
|
|
183
|
+
}
|
|
184
|
+
else if (year <= 1954) {
|
|
185
|
+
return 66 * 12;
|
|
186
|
+
}
|
|
187
|
+
else if (year <= 1959) {
|
|
188
|
+
// Gradual increase from 66 years to 66 years 10 months
|
|
189
|
+
return ((year - 1954) * 2) + (66 * 12);
|
|
190
|
+
}
|
|
191
|
+
else if (year >= 1960) {
|
|
192
|
+
return 67 * 12;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
throw new Error(`Invalid birth year: ${year}`);
|
|
165
196
|
}
|
|
166
197
|
}
|
|
167
198
|
function monthsDifference(date1, date2) {
|
|
168
|
-
// Calculate the total number of months for each date
|
|
169
199
|
const months1 = date1.getFullYear() * 12 + date1.getMonth();
|
|
170
200
|
const months2 = date2.getFullYear() * 12 + date2.getMonth();
|
|
171
|
-
// Return the difference in months
|
|
172
201
|
return months1 - months2;
|
|
173
202
|
}
|
package/lib/model.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type Earnings = {
|
|
2
2
|
year: number;
|
|
3
3
|
earnings: number;
|
|
4
4
|
}[];
|
|
5
|
+
export type Wage = {
|
|
6
|
+
year: number;
|
|
7
|
+
retirement: number;
|
|
8
|
+
disability: number;
|
|
9
|
+
survivors: number;
|
|
10
|
+
taxMax: number;
|
|
11
|
+
cola: number;
|
|
12
|
+
awi: number;
|
|
13
|
+
};
|
|
14
|
+
export type Wages = Wage[];
|
|
15
|
+
export interface BenefitCalculationResult {
|
|
16
|
+
AIME: number;
|
|
17
|
+
NormalMonthlyBenefit: number;
|
|
18
|
+
}
|
|
19
|
+
export interface RetirementDates {
|
|
20
|
+
earliestRetirement: Date;
|
|
21
|
+
fullRetirement: Date;
|
|
22
|
+
maxRetirement: Date;
|
|
23
|
+
adjusted: Date;
|
|
24
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
declare function getWages(fileName: string): Promise<
|
|
1
|
+
import { Earnings } from '../model';
|
|
2
|
+
declare function getWages(fileName: string): Promise<Earnings>;
|
|
3
3
|
export default getWages;
|
package/lib/wage-index.d.ts
CHANGED