social-security-calculator 0.0.7 → 0.0.8

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.
@@ -1,8 +1,7 @@
1
- import { wageIndex } from '../wage-index';
1
+ import { wageIndex, quickCalcProjections, taxableMaximum } from '../wage-index';
2
2
  const YOUTH_FACTOR = 8;
3
3
  const YOUTH_FACTOR_AGE = 21;
4
4
  const WORK_START_AGE = 18;
5
- const CURRENT_YEAR_INCREASE = .945; //.96
6
5
  const CURRENT_YEAR = new Date().getFullYear();
7
6
  export function getEstimatedEarnings(age, lastWage, lastYearWorked = CURRENT_YEAR, earningGrowthRate = 0) {
8
7
  if (age <= 22) {
@@ -14,36 +13,33 @@ export function getEstimatedEarnings(age, lastWage, lastYearWorked = CURRENT_YEA
14
13
  const workStartYear = CURRENT_YEAR - age + WORK_START_AGE;
15
14
  const yearTurned22 = CURRENT_YEAR - age + YOUTH_FACTOR_AGE;
16
15
  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;
16
+ for (let i = lastYearWorked; i >= workStartYear; i--) {
21
17
  const year = i;
22
18
  const nextYear = (i + 1);
23
- if (i === lastYearWorked) {
24
- wageResults[year] = lastWage;
25
- }
26
- else {
27
- wageResults[year] = (wageResults[nextYear] * reductionFactor) / youthFactor;
28
- }
19
+ const youthAdjustment = (i === yearTurned22 ? YOUTH_FACTOR : 1);
20
+ wageResults[year] = (i === lastYearWorked)
21
+ ? lastWage
22
+ : wageResults[nextYear] * getReductionFactor(i) / (1 + earningGrowthRate) / youthAdjustment;
29
23
  }
24
+ // Cap wages at taxable maximum
25
+ Object.keys(wageResults).forEach(strYear => {
26
+ const year = parseInt(strYear);
27
+ const maxTaxable = taxableMaximum[year];
28
+ wageResults[year] = Math.min(wageResults[year], maxTaxable);
29
+ });
30
30
  return wageResults;
31
31
  }
32
32
  function getReductionFactor(year) {
33
+ const allIndexes = Object.assign(Object.assign({}, wageIndex), quickCalcProjections);
33
34
  const lastYear = year - 1;
34
35
  const nextYear = year + 1;
35
- if (year === CURRENT_YEAR && !wageIndex[lastYear]) {
36
+ if (year === CURRENT_YEAR && !allIndexes[lastYear]) {
36
37
  throw new Error(`Wage index for previous year (${lastYear}) is required`);
37
- // return CURRENT_YEAR_INCREASE
38
38
  }
39
39
  if (year === CURRENT_YEAR) {
40
40
  return 1;
41
41
  }
42
- else if (year === CURRENT_YEAR - 1) {
43
- return CURRENT_YEAR_INCREASE;
44
- }
45
42
  else {
46
- return (wageIndex[year] / wageIndex[nextYear]);
43
+ return (allIndexes[year] / allIndexes[nextYear]);
47
44
  }
48
45
  }
49
- // console.log(getEstimatedEarnings(70, 100000, 2020, .02));
package/lib/index.js CHANGED
@@ -1,29 +1,34 @@
1
1
  import { wageIndex } from './wage-index';
2
- export const calc = (earnings) => {
3
- const lookbackYears = 35;
4
- const futureYearsFactor = 1;
2
+ export function calc(birthday, retirementDate, earnings) {
3
+ const AIME = calculateAIME(earnings);
4
+ const results = calcRetirementBenefit(birthday, retirementDate, AIME);
5
+ return results;
6
+ }
7
+ export function calcRetirementBenefit(birthday, retirementDate, AIME) {
8
+ const eclBirthDate = getEnglishCommonLawDate(birthday);
9
+ const fullRetirementMonths = getFullRetirementMonths(eclBirthDate);
10
+ const PIA = calculatePIA(AIME);
11
+ const benefitAt62 = calculateSocialSecurityReduction(PIA, 60);
12
+ const normalMonthlyBenefit = Math.floor(PIA);
13
+ const results = {
14
+ "AIME": AIME,
15
+ "NormalMonthlyBenefit": normalMonthlyBenefit,
16
+ "NormalAnnualBenefit": PIA * 12,
17
+ "ReducedMonthlyBenefit": benefitAt62,
18
+ "ReducedAnnualBenefit": benefitAt62 * 12,
19
+ };
20
+ return results;
21
+ }
22
+ function calculatePIA(AIME) {
23
+ const averageWageLastYear = Math.max(...Object.keys(wageIndex).map(val => parseInt(val)));
24
+ const wageIndexLastYear = wageIndex[averageWageLastYear];
5
25
  const bendPointDivisor = 9779.44;
6
26
  const firstBendPointMultiplier = 180;
7
27
  const secondBendPointMultiplier = 1085;
8
- const averageWageLastYear = Math.max(...Object.keys(wageIndex).map(val => parseInt(val)));
9
- const wageIndexLastYear = wageIndex[averageWageLastYear];
10
28
  // https://www.ssa.gov/oact/COLA/piaformula.html
11
29
  // Per examples, bend points are rounded to the nearest dollar
12
30
  const firstBendPoint = Math.round(firstBendPointMultiplier * wageIndexLastYear / bendPointDivisor);
13
31
  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)
20
- .sort((a, b) => b - a) // sort the earnings from highest to lowest amount
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
32
  // https://www.ssa.gov/OP_Home/handbook/handbook.07/handbook-0738.html
28
33
  // 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
34
  const PIA = (() => {
@@ -33,28 +38,48 @@ export const calc = (earnings) => {
33
38
  }
34
39
  else {
35
40
  if (AIME > firstBendPoint && AIME <= secondBendPoint) {
36
- monthlyBenefit = 0.9 * firstBendPoint + 0.32 * (AIME - firstBendPoint);
41
+ monthlyBenefit = (0.9 * firstBendPoint) + (0.32 * (AIME - firstBendPoint));
37
42
  }
38
43
  else {
39
- monthlyBenefit = 0.9 * firstBendPoint + 0.32 * (secondBendPoint - firstBendPoint) + 0.15 * (AIME - secondBendPoint);
44
+ monthlyBenefit = (0.9 * firstBendPoint) + (0.32 * (secondBendPoint - firstBendPoint)) + (0.15 * (AIME - secondBendPoint));
40
45
  }
41
46
  }
42
47
  return roundToFloorTenCents(monthlyBenefit);
43
48
  })();
44
- const reducedMonthlyBenefit = Math.floor(0.7 * PIA);
45
- const normalMonthlyBenefit = Math.floor(PIA);
46
- const results = {
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,
55
- };
56
- return results;
57
- };
49
+ return PIA;
50
+ }
51
+ function calculateAIME(earnings) {
52
+ const lookbackYears = 35;
53
+ const averageWageLastYear = Math.max(...Object.keys(wageIndex).map(val => parseInt(val)));
54
+ const wageIndexLastYear = wageIndex[averageWageLastYear];
55
+ const futureYearsFactor = 1;
56
+ // calculate the wage index factors
57
+ const wageIndexFactors = Object.entries(wageIndex).reduce((acc, [i, val]) => ((acc[parseInt(i)] = 1 + (wageIndexLastYear - val) / val), acc), {});
58
+ // adjust the earnings according to the wage index factor,
59
+ // factor is 1 for any earnings record without a wage-factor
60
+ const adjustedEarnings = Object.entries(earnings).reduce((acc, [i, val]) => ((acc[parseInt(i)] = val * (wageIndexFactors[parseInt(i)] || futureYearsFactor)), acc), {});
61
+ const top35YearsEarningsArr = Object.values(adjustedEarnings)
62
+ .sort((a, b) => b - a) // sort the earnings from highest to lowest amount
63
+ .slice(0, lookbackYears); // grab the highest 35 earnings years
64
+ const top35YearsEarnings = top35YearsEarningsArr.reduce((partialSum, a) => partialSum + a, 0); // and finally sum them
65
+ // https://www.ssa.gov/oact/cola/Benefits.html
66
+ // "We then round the resulting average amount down to the next lower dollar amount"
67
+ const AIME = Math.floor(top35YearsEarnings / (12 * lookbackYears));
68
+ return AIME;
69
+ }
70
+ function calculateSocialSecurityReduction(amount, months) {
71
+ const first36Rate = 5 / 9 * 0.01; // 5/9 of 1%
72
+ const additionalRate = 5 / 12 * 0.01; // 5/12 of 1%
73
+ let reduction = 0;
74
+ if (months <= 36) {
75
+ reduction = months * first36Rate;
76
+ }
77
+ else {
78
+ // 36 months times 5/9 of 1 percent plus 24 months times 5/12 of 1 percent.
79
+ reduction = 36 * first36Rate + (months - 36) * additionalRate;
80
+ }
81
+ return amount - amount * reduction;
82
+ }
58
83
  function roundToFloorTenCents(amount) {
59
84
  // Convert the amount to fractional dimes
60
85
  let dimes = amount * 10;
@@ -63,3 +88,33 @@ function roundToFloorTenCents(amount) {
63
88
  // Convert back to dollars and return
64
89
  return (dimes / 10);
65
90
  }
91
+ function getEnglishCommonLawDate(date) {
92
+ const year = date.getFullYear();
93
+ const month = date.getMonth();
94
+ const day = date.getDate();
95
+ const englishCommonLawDate = new Date(year, month, day - 1);
96
+ return englishCommonLawDate;
97
+ }
98
+ export function getFullRetirementMonths(commonLawBirthDate) {
99
+ const year = commonLawBirthDate.getFullYear();
100
+ switch (true) {
101
+ case (year >= 1943 && year <= 1954):
102
+ return 66 * 12;
103
+ case (year >= 1955 && year <= 1959):
104
+ const fra = ((year - 1954) * 2) + (66 * 12);
105
+ return fra;
106
+ case (year >= 1960):
107
+ return 67 * 12;
108
+ default:
109
+ throw new Error('Invalid birth year');
110
+ }
111
+ }
112
+ /*
113
+ 1943-1954 66 48 $750 25.00% $350 30.00%
114
+ 1955 66 and 2 months 50 $741 25.83% $345 30.83%
115
+ 1956 66 and 4 months 52 $733 26.67% $341 31.67%
116
+ 1957 66 and 6 months 54 $725 27.50% $337 32.50%
117
+ 1958 66 and 8 months 56 $716 28.33% $333 33.33%
118
+ 1959 66 and 10 months 58 $708 29.17% $329 34.17%
119
+ 1960+ 67 60 $700 30.00% $325 35.00%
120
+ */
@@ -1,17 +1,19 @@
1
1
  import xml2js from 'xml2js';
2
2
  import fs from 'fs/promises';
3
- const NS = 'osss';
4
- const parser = new xml2js.Parser();
3
+ const NS = 'OSSS';
4
+ // strict false because attribute is unquoted: <osss:OnlineSocialSecurityStatementData xmlns:osss=http://ssa.gov/osss/schemas/2.0>
5
+ // could fix the XML, but better to keep file unchanged so that we can drop in relacement files from SSA
6
+ const parser = new xml2js.Parser({ strict: false });
5
7
  const supportedVersion = "http://ssa.gov/osss/schemas/2.0";
6
8
  async function getWages(fileName) {
7
9
  const data = await fs.readFile(fileName);
8
10
  const result = await parser.parseStringPromise(data);
9
- const schema = result[`${NS}:OnlineSocialSecurityStatementData`]['$'][`xmlns:${NS}`];
11
+ const schema = result[`${NS}:ONLINESOCIALSECURITYSTATEMENTDATA`]['$'][`XMLNS:${NS}`];
10
12
  if (schema !== supportedVersion) {
11
13
  throw `${schema} is not supported (${supportedVersion})`;
12
14
  }
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
+ const results = result[`${NS}:ONLINESOCIALSECURITYSTATEMENTDATA`][`${NS}:EARNINGSRECORD`][0][`${NS}:EARNINGS`];
16
+ const earnings = results.reduce((acc, earn) => (earn[`${NS}:FICAEARNINGS`][0] === '-1' ? acc : acc[earn['$'].STARTYEAR] = parseInt(earn[`${NS}:FICAEARNINGS`][0]), acc), {});
15
17
  return earnings;
16
18
  }
17
19
  export default getWages;
package/lib/wage-index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { compound } from 'compound-calc';
2
1
  // https://www.ssa.gov/OACT/COLA/awiseries.html
3
2
  export const wageIndex = {
4
3
  1951: 2799.16,
@@ -74,47 +73,88 @@ export const wageIndex = {
74
73
  2021: 60575.07,
75
74
  2022: 63795.13
76
75
  };
77
- const shortRangeIntermediate = {
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,
76
+ // retrieved from: view-source:https://www.ssa.gov/cgi-bin/benefit6.cgi
77
+ export const quickCalcProjections = {
78
+ 2023: 66488.13,
79
+ 2024: 68981.33,
80
+ 2025: 71780.09,
81
+ 2026: 74731.71,
82
+ 2027: 77792.60,
83
+ 2028: 80925.73,
84
+ 2029: 84188.76,
85
+ 2030: 87555.49,
86
+ 2031: 91041.35,
87
+ 2032: 94479.84,
88
+ 2033: 97957.36,
89
+ 2034: 101547.32,
90
+ 2035: 105266.09,
91
+ 2036: 109120.74,
92
+ 2037: 113117.42,
93
+ 2038: 117250.70,
94
+ 2039: 121537.41,
95
+ 2040: 125958.59,
96
+ 2041: 130512.20,
97
+ 2042: 135207.00,
98
+ 2043: 140051.90,
99
+ 2044: 145045.39,
100
+ 2045: 150193.68,
101
+ 2046: 155501.36,
102
+ 2047: 160999.09,
103
+ 2048: 166686.37,
104
+ 2049: 172576.11,
88
105
  };
89
- const longRangeIntermediate = {
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,
106
+ export const taxableMaximum = {
107
+ 1972: 9000,
108
+ 1973: 10800,
109
+ 1974: 13200,
110
+ 1975: 14100,
111
+ 1976: 15300,
112
+ 1977: 16500,
113
+ 1978: 17700,
114
+ 1979: 22900,
115
+ 1980: 25900,
116
+ 1981: 29700,
117
+ 1982: 32400,
118
+ 1983: 35700,
119
+ 1984: 37800,
120
+ 1985: 39600,
121
+ 1986: 42000,
122
+ 1987: 43800,
123
+ 1988: 45000,
124
+ 1989: 48000,
125
+ 1990: 51300,
126
+ 1991: 53400,
127
+ 1992: 55500,
128
+ 1993: 57600,
129
+ 1994: 60600,
130
+ 1995: 61200,
131
+ 1996: 62700,
132
+ 1997: 65400,
133
+ 1998: 68400,
134
+ 1999: 72600,
135
+ 2000: 76200,
136
+ 2001: 80400,
137
+ 2002: 84900,
138
+ 2003: 87000,
139
+ 2004: 87900,
140
+ 2005: 90000,
141
+ 2006: 94200,
142
+ 2007: 97500,
143
+ 2008: 102000,
144
+ 2009: 106800,
145
+ 2010: 106800,
146
+ 2011: 106800,
147
+ 2012: 110100,
148
+ 2013: 113700,
149
+ 2014: 117000,
150
+ 2015: 118500,
151
+ 2016: 118500,
152
+ 2017: 127200,
153
+ 2018: 128400,
154
+ 2019: 132900,
155
+ 2020: 137700,
156
+ 2021: 142800,
157
+ 2022: 147000,
158
+ 2023: 160200,
159
+ 2024: 168600,
104
160
  };
105
- const fillBlanks = (vals) => Object.entries(vals).reduce((acc, [year, P], index, arr) => {
106
- let thing;
107
- const iYear = parseInt(year);
108
- if (arr[index + 1]) {
109
- const [nextYear, A] = arr[index + 1];
110
- const t = parseInt(nextYear) - parseInt(year);
111
- const r = Math.pow((A / P), (1 / t)) - 1;
112
- const vals = compound(P, 0, t, r).result.slice(0, -1);
113
- const ret = vals.reduce((accx, cur, i) => ((accx[iYear + i] = cur) && accx), {});
114
- thing = Object.assign(Object.assign({}, acc), ret);
115
- }
116
- else {
117
- thing = Object.assign(Object.assign({}, acc), { [year]: P });
118
- }
119
- return thing;
120
- }, {});
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "social-security-calculator",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Calculate estimated Social Security Benefits",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "type": "module",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./parseStatement": "./lib/parseStatement/index.js",
11
+ "./estimatedEarnings": "./lib/estimatedEarnings/index.js"
12
+ },
8
13
  "scripts": {
9
14
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
10
15
  "build": "tsc"