social-security-calculator 0.0.6 → 0.0.7
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/LICENSE.txt +13 -0
- package/README.md +5 -8
- package/lib/estimatedEarnings/index.js +49 -0
- package/lib/index.js +44 -28
- package/lib/model.js +1 -0
- package/lib/parseStatement/index.js +17 -0
- package/lib/wage-index.js +31 -33
- package/package.json +17 -5
- package/lib/index.d.ts +0 -15
- package/lib/package.bak +0 -31
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2022 RYAN ANTKOWIAK, CHARLES MCNULTY
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
|
|
4
|
+
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
|
|
5
|
+
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
6
|
+
Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
11
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
|
12
|
+
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
|
13
|
+
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,14 +4,11 @@ TypeScript to calculate estimated Social Security Benefits
|
|
|
4
4
|
|
|
5
5
|
This module will calculate your expected retirement benefits
|
|
6
6
|
from Social Security given your annual earnings
|
|
7
|
-
Inputs:
|
|
8
|
-
1) EarningsRecord -
|
|
9
|
-
Dictionary mapping a year to the amount of Social
|
|
10
|
-
Security eligible earnings in that particular year
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
Data pulled directly from the Social Security website for the
|
|
14
|
-
national average wage data
|
|
8
|
+
Input:
|
|
15
9
|
|
|
16
|
-
|
|
10
|
+
Dictionary mapping a year to the amount of Social
|
|
11
|
+
Security eligible earnings in that particular year
|
|
17
12
|
|
|
13
|
+
|
|
14
|
+
Adapted from python originally written by Ryan Antkowiak
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { wageIndex } from '../wage-index';
|
|
2
|
+
const YOUTH_FACTOR = 8;
|
|
3
|
+
const YOUTH_FACTOR_AGE = 21;
|
|
4
|
+
const WORK_START_AGE = 18;
|
|
5
|
+
const CURRENT_YEAR_INCREASE = .945; //.96
|
|
6
|
+
const CURRENT_YEAR = new Date().getFullYear();
|
|
7
|
+
export function getEstimatedEarnings(age, lastWage, lastYearWorked = CURRENT_YEAR, earningGrowthRate = 0) {
|
|
8
|
+
if (age <= 22) {
|
|
9
|
+
throw new Error('Age must be greater than 22');
|
|
10
|
+
}
|
|
11
|
+
if (lastYearWorked > CURRENT_YEAR) {
|
|
12
|
+
throw new Error('Last year worked cannot be in the future');
|
|
13
|
+
}
|
|
14
|
+
const workStartYear = CURRENT_YEAR - age + WORK_START_AGE;
|
|
15
|
+
const yearTurned22 = CURRENT_YEAR - age + YOUTH_FACTOR_AGE;
|
|
16
|
+
const wageResults = {};
|
|
17
|
+
let i = lastYearWorked;
|
|
18
|
+
for (; i >= workStartYear; i--) {
|
|
19
|
+
const reductionFactor = getReductionFactor(i) / (1 + earningGrowthRate);
|
|
20
|
+
const youthFactor = i === yearTurned22 ? YOUTH_FACTOR : 1;
|
|
21
|
+
const year = i;
|
|
22
|
+
const nextYear = (i + 1);
|
|
23
|
+
if (i === lastYearWorked) {
|
|
24
|
+
wageResults[year] = lastWage;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
wageResults[year] = (wageResults[nextYear] * reductionFactor) / youthFactor;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return wageResults;
|
|
31
|
+
}
|
|
32
|
+
function getReductionFactor(year) {
|
|
33
|
+
const lastYear = year - 1;
|
|
34
|
+
const nextYear = year + 1;
|
|
35
|
+
if (year === CURRENT_YEAR && !wageIndex[lastYear]) {
|
|
36
|
+
throw new Error(`Wage index for previous year (${lastYear}) is required`);
|
|
37
|
+
// return CURRENT_YEAR_INCREASE
|
|
38
|
+
}
|
|
39
|
+
if (year === CURRENT_YEAR) {
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
else if (year === CURRENT_YEAR - 1) {
|
|
43
|
+
return CURRENT_YEAR_INCREASE;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return (wageIndex[year] / wageIndex[nextYear]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// console.log(getEstimatedEarnings(70, 100000, 2020, .02));
|
package/lib/index.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const wage_index_1 = require("./wage-index");
|
|
4
|
-
function calc(earnings) {
|
|
1
|
+
import { wageIndex } from './wage-index';
|
|
2
|
+
export const calc = (earnings) => {
|
|
5
3
|
const lookbackYears = 35;
|
|
6
4
|
const futureYearsFactor = 1;
|
|
7
5
|
const bendPointDivisor = 9779.44;
|
|
8
|
-
const firstBendPointMultiplier = 180
|
|
9
|
-
const secondBendPointMultiplier = 1085
|
|
10
|
-
const averageWageLastYear = Math.max(...Object.keys(
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
6
|
+
const firstBendPointMultiplier = 180;
|
|
7
|
+
const secondBendPointMultiplier = 1085;
|
|
8
|
+
const averageWageLastYear = Math.max(...Object.keys(wageIndex).map(val => parseInt(val)));
|
|
9
|
+
const wageIndexLastYear = wageIndex[averageWageLastYear];
|
|
10
|
+
// https://www.ssa.gov/oact/COLA/piaformula.html
|
|
11
|
+
// Per examples, bend points are rounded to the nearest dollar
|
|
12
|
+
const firstBendPoint = Math.round(firstBendPointMultiplier * wageIndexLastYear / bendPointDivisor);
|
|
13
|
+
const secondBendPoint = Math.round(secondBendPointMultiplier * wageIndexLastYear / bendPointDivisor);
|
|
14
|
+
// calculate the wage index factors
|
|
15
|
+
const wageIndexFactors = Object.entries(wageIndex).reduce((acc, [i, val]) => ((acc[parseInt(i)] = 1 + (wageIndexLastYear - val) / val), acc), {});
|
|
16
|
+
// adjust the earnings according to the wage index factor,
|
|
17
|
+
// factor is 1 for any earnings record without a wage-factor
|
|
18
|
+
const adjustedEarnings = Object.entries(earnings).reduce((acc, [i, val]) => ((acc[parseInt(i)] = val * (wageIndexFactors[parseInt(i)] || futureYearsFactor)), acc), {});
|
|
19
|
+
const top35YearsEarningsArr = Object.values(adjustedEarnings)
|
|
16
20
|
.sort((a, b) => b - a) // sort the earnings from highest to lowest amount
|
|
17
|
-
.slice(0, lookbackYears) // grab the highest 35 earnings years
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
+
.slice(0, lookbackYears); // grab the highest 35 earnings years
|
|
22
|
+
console.log(top35YearsEarningsArr);
|
|
23
|
+
const top35YearsEarnings = top35YearsEarningsArr.reduce((partialSum, a) => partialSum + a, 0); // and finally sum them
|
|
24
|
+
// https://www.ssa.gov/oact/cola/Benefits.html
|
|
25
|
+
// "We then round the resulting average amount down to the next lower dollar amount"
|
|
26
|
+
const AIME = Math.floor(top35YearsEarnings / (12 * lookbackYears));
|
|
27
|
+
// https://www.ssa.gov/OP_Home/handbook/handbook.07/handbook-0738.html
|
|
28
|
+
// Calculations that are not a multiple of 10 cents are rounded to the next lower multiple of 10 cents. For example, $100.18 is rounded down to $100.10.
|
|
29
|
+
const PIA = (() => {
|
|
21
30
|
let monthlyBenefit = 0;
|
|
22
31
|
if (AIME <= firstBendPoint) {
|
|
23
32
|
monthlyBenefit = 0.9 * AIME;
|
|
@@ -30,20 +39,27 @@ function calc(earnings) {
|
|
|
30
39
|
monthlyBenefit = 0.9 * firstBendPoint + 0.32 * (secondBendPoint - firstBendPoint) + 0.15 * (AIME - secondBendPoint);
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
|
-
return
|
|
34
|
-
;
|
|
42
|
+
return roundToFloorTenCents(monthlyBenefit);
|
|
35
43
|
})();
|
|
36
|
-
const reducedMonthlyBenefit = Math.floor(
|
|
44
|
+
const reducedMonthlyBenefit = Math.floor(0.7 * PIA);
|
|
45
|
+
const normalMonthlyBenefit = Math.floor(PIA);
|
|
37
46
|
const results = {
|
|
38
|
-
"Top35YearsEarnings": top35YearsEarnings
|
|
39
|
-
"AIME": AIME
|
|
40
|
-
"FirstBendPoint": firstBendPoint
|
|
41
|
-
"SecondBendPoint": secondBendPoint
|
|
42
|
-
"NormalMonthlyBenefit": normalMonthlyBenefit
|
|
43
|
-
"NormalAnnualBenefit":
|
|
44
|
-
"ReducedMonthlyBenefit": reducedMonthlyBenefit
|
|
45
|
-
"ReducedAnnualBenefit":
|
|
47
|
+
"Top35YearsEarnings": top35YearsEarnings,
|
|
48
|
+
"AIME": AIME,
|
|
49
|
+
"FirstBendPoint": firstBendPoint,
|
|
50
|
+
"SecondBendPoint": secondBendPoint,
|
|
51
|
+
"NormalMonthlyBenefit": normalMonthlyBenefit,
|
|
52
|
+
"NormalAnnualBenefit": PIA * 12,
|
|
53
|
+
"ReducedMonthlyBenefit": reducedMonthlyBenefit,
|
|
54
|
+
"ReducedAnnualBenefit": reducedMonthlyBenefit * 12,
|
|
46
55
|
};
|
|
47
56
|
return results;
|
|
57
|
+
};
|
|
58
|
+
function roundToFloorTenCents(amount) {
|
|
59
|
+
// Convert the amount to fractional dimes
|
|
60
|
+
let dimes = amount * 10;
|
|
61
|
+
// floor to only whole dimes
|
|
62
|
+
dimes = Math.floor(dimes);
|
|
63
|
+
// Convert back to dollars and return
|
|
64
|
+
return (dimes / 10);
|
|
48
65
|
}
|
|
49
|
-
module.exports = calc;
|
package/lib/model.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import xml2js from 'xml2js';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
const NS = 'osss';
|
|
4
|
+
const parser = new xml2js.Parser();
|
|
5
|
+
const supportedVersion = "http://ssa.gov/osss/schemas/2.0";
|
|
6
|
+
async function getWages(fileName) {
|
|
7
|
+
const data = await fs.readFile(fileName);
|
|
8
|
+
const result = await parser.parseStringPromise(data);
|
|
9
|
+
const schema = result[`${NS}:OnlineSocialSecurityStatementData`]['$'][`xmlns:${NS}`];
|
|
10
|
+
if (schema !== supportedVersion) {
|
|
11
|
+
throw `${schema} is not supported (${supportedVersion})`;
|
|
12
|
+
}
|
|
13
|
+
const results = result[`${NS}:OnlineSocialSecurityStatementData`][`${NS}:EarningsRecord`][0][`${NS}:Earnings`];
|
|
14
|
+
const earnings = results.reduce((acc, earn) => (acc[earn['$'].startYear] = parseInt(earn[`${NS}:FicaEarnings`][0]), acc), {});
|
|
15
|
+
return earnings;
|
|
16
|
+
}
|
|
17
|
+
export default getWages;
|
package/lib/wage-index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const compound = require('compound-calc');
|
|
5
|
-
exports.wageIndex = {
|
|
1
|
+
import { compound } from 'compound-calc';
|
|
2
|
+
// https://www.ssa.gov/OACT/COLA/awiseries.html
|
|
3
|
+
export const wageIndex = {
|
|
6
4
|
1951: 2799.16,
|
|
7
5
|
1952: 2973.32,
|
|
8
6
|
1953: 3139.44,
|
|
@@ -73,44 +71,46 @@ exports.wageIndex = {
|
|
|
73
71
|
2018: 52145.8,
|
|
74
72
|
2019: 54099.99,
|
|
75
73
|
2020: 55628.6,
|
|
74
|
+
2021: 60575.07,
|
|
75
|
+
2022: 63795.13
|
|
76
76
|
};
|
|
77
77
|
const shortRangeIntermediate = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
2031: 88836.46,
|
|
78
|
+
2023: 66147.17,
|
|
79
|
+
2024: 68627.58,
|
|
80
|
+
2025: 71411.99,
|
|
81
|
+
2026: 74348.48,
|
|
82
|
+
2027: 77393.67,
|
|
83
|
+
2028: 80510.73,
|
|
84
|
+
2029: 83757.03,
|
|
85
|
+
2030: 87106.49,
|
|
86
|
+
2031: 90574.48,
|
|
87
|
+
2032: 93995.33,
|
|
89
88
|
};
|
|
90
89
|
const longRangeIntermediate = {
|
|
91
|
-
2035:
|
|
92
|
-
2040:
|
|
93
|
-
2045:
|
|
94
|
-
2050:
|
|
95
|
-
2055:
|
|
96
|
-
2060:
|
|
97
|
-
2065:
|
|
98
|
-
2070:
|
|
99
|
-
2075:
|
|
100
|
-
2080:
|
|
101
|
-
2085:
|
|
102
|
-
2090:
|
|
103
|
-
2095:
|
|
104
|
-
2100:
|
|
90
|
+
2035: 104726.27,
|
|
91
|
+
2040: 125312.66,
|
|
92
|
+
2045: 149423.47,
|
|
93
|
+
2050: 177750.26,
|
|
94
|
+
2055: 211432.09,
|
|
95
|
+
2060: 251610.19,
|
|
96
|
+
2065: 299758.28,
|
|
97
|
+
2070: 357187.25,
|
|
98
|
+
2075: 425523.96,
|
|
99
|
+
2080: 506962.67,
|
|
100
|
+
2085: 603863.51,
|
|
101
|
+
2090: 719124.11,
|
|
102
|
+
2095: 856091.73,
|
|
103
|
+
2100: 1019162.94,
|
|
105
104
|
};
|
|
106
105
|
const fillBlanks = (vals) => Object.entries(vals).reduce((acc, [year, P], index, arr) => {
|
|
107
106
|
let thing;
|
|
107
|
+
const iYear = parseInt(year);
|
|
108
108
|
if (arr[index + 1]) {
|
|
109
109
|
const [nextYear, A] = arr[index + 1];
|
|
110
110
|
const t = parseInt(nextYear) - parseInt(year);
|
|
111
111
|
const r = Math.pow((A / P), (1 / t)) - 1;
|
|
112
112
|
const vals = compound(P, 0, t, r).result.slice(0, -1);
|
|
113
|
-
const ret = vals.reduce((accx, cur, i) => ((accx[
|
|
113
|
+
const ret = vals.reduce((accx, cur, i) => ((accx[iYear + i] = cur) && accx), {});
|
|
114
114
|
thing = Object.assign(Object.assign({}, acc), ret);
|
|
115
115
|
}
|
|
116
116
|
else {
|
|
@@ -118,5 +118,3 @@ const fillBlanks = (vals) => Object.entries(vals).reduce((acc, [year, P], index,
|
|
|
118
118
|
}
|
|
119
119
|
return thing;
|
|
120
120
|
}, {});
|
|
121
|
-
// console.log(fillBlanks(longRangeIntermediate));
|
|
122
|
-
console.log(fillBlanks({ 2000: 38915, 2021: 108494 }));
|
package/package.json
CHANGED
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-security-calculator",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Calculate estimated Social Security Benefits",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"scripts": {
|
|
8
|
-
"test": "
|
|
9
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
9
10
|
"build": "tsc"
|
|
10
11
|
},
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
|
13
14
|
"url": "git+https://github.com/cmcnulty/SocialSecurityCalculator.git"
|
|
14
15
|
},
|
|
15
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"calculator",
|
|
18
|
+
"social",
|
|
19
|
+
"security",
|
|
20
|
+
"ssi"
|
|
21
|
+
],
|
|
16
22
|
"author": "",
|
|
17
|
-
"license": "
|
|
23
|
+
"license": "MIT",
|
|
18
24
|
"bugs": {
|
|
19
25
|
"url": "https://github.com/cmcnulty/SocialSecurityCalculator/issues"
|
|
20
26
|
},
|
|
21
27
|
"homepage": "https://github.com/cmcnulty/SocialSecurityCalculator#readme",
|
|
22
28
|
"devDependencies": {
|
|
29
|
+
"@jest/globals": "^29.7.0",
|
|
30
|
+
"@types/jest": "^29.5.8",
|
|
31
|
+
"@types/xml2js": "^0.4.11",
|
|
32
|
+
"compound-calc": "^4.0.3",
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"ts-jest": "^29.1.1",
|
|
23
35
|
"typescript": "^4.8.3"
|
|
24
36
|
},
|
|
25
37
|
"dependencies": {
|
|
26
|
-
"
|
|
38
|
+
"xml2js": "^0.6.2"
|
|
27
39
|
},
|
|
28
40
|
"files": [
|
|
29
41
|
"lib"
|
package/lib/index.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export = calc;
|
|
2
|
-
declare function calc(earnings: any): {
|
|
3
|
-
Top35YearsEarnings: any;
|
|
4
|
-
AIME: string;
|
|
5
|
-
FirstBendPoint: string;
|
|
6
|
-
SecondBendPoint: string;
|
|
7
|
-
NormalMonthlyBenefit: string;
|
|
8
|
-
NormalAnnualBenefit: string;
|
|
9
|
-
ReducedMonthlyBenefit: string;
|
|
10
|
-
ReducedAnnualBenefit: string;
|
|
11
|
-
};
|
|
12
|
-
declare namespace calc {
|
|
13
|
-
export { __esModule };
|
|
14
|
-
}
|
|
15
|
-
declare const __esModule: boolean;
|
package/lib/package.bak
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "social-security-calculator",
|
|
3
|
-
"version": "0.0.5",
|
|
4
|
-
"description": "Calculate estimated Social Security Benefits",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"types": "index.d.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"**/*"
|
|
9
|
-
],
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
-
"build": "tsc"
|
|
13
|
-
},
|
|
14
|
-
"repository": {
|
|
15
|
-
"type": "git",
|
|
16
|
-
"url": "git+https://github.com/cmcnulty/SocialSecurityCalculator.git"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [],
|
|
19
|
-
"author": "",
|
|
20
|
-
"license": "ISC",
|
|
21
|
-
"bugs": {
|
|
22
|
-
"url": "https://github.com/cmcnulty/SocialSecurityCalculator/issues"
|
|
23
|
-
},
|
|
24
|
-
"homepage": "https://github.com/cmcnulty/SocialSecurityCalculator#readme",
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"typescript": "^4.8.3"
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"compound-calc": "^2.0.0"
|
|
30
|
-
}
|
|
31
|
-
}
|