Extract constant values from fed2019 Forms as named definitions.
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 2 Aug 2020 16:07:09 +0000 (12:07 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 18 Oct 2020 15:59:14 +0000 (11:59 -0400)
The core/TaxReturn has a new abstract constants property that can be
used to inject values. Forms can then reference the values when
performing computations, without hard-coding them.

This also adds a Literal identify function to core/Math, to make it
easier to find constants that won't vary from year to year.

19 files changed:
src/core/Form.test.ts
src/core/Line.test.ts
src/core/Math.ts
src/core/TaxReturn.test.ts
src/core/TaxReturn.ts
src/core/Trace.test.ts
src/fed2019/Form1040.ts
src/fed2019/Form6251.ts
src/fed2019/Form8959.test.ts
src/fed2019/Form8959.ts
src/fed2019/Form8960.test.ts
src/fed2019/Form8960.ts
src/fed2019/Form8995.ts
src/fed2019/Schedule1.ts
src/fed2019/Schedule2.ts
src/fed2019/Schedule3.ts
src/fed2019/ScheduleA.ts
src/fed2019/ScheduleD.ts
src/fed2019/TaxReturn.ts

index 5b00536c05e4553ff7a685be69b6a4bf5fdd6ccd..9021d60dd2c254c0375a1664ad35c135870d8b55 100644 (file)
@@ -9,6 +9,7 @@ import Form, { isFormT } from './Form';
 import { InconsistencyError, NotFoundError } from './Errors';
 
 class TestTaxReturn extends TaxReturn {
+  readonly constants = undefined;
   get year() { return 2019; }
   get includeJointPersonForms() { return true; }
 };
index 4a596389171f15c49aa0bd57068a2998619e7450..df960c86d3567a75f8b46f4acea935bfc0b432c5 100644 (file)
@@ -9,6 +9,7 @@ import TaxReturn from './TaxReturn';
 import { NotFoundError } from './Errors';
 
 class TestTaxReturn extends TaxReturn {
+  readonly constants = undefined;
   get year() { return 2019; }
   get includeJointPersonForms() { return false; }
 };
index dd921d5a9205b026f9d2350325625a92d68ce45c..ce14b7e02ac70a1bc5c0e15de68f477d7e1c9e29 100644 (file)
@@ -8,3 +8,7 @@ export const clampToZero = (value: number): number => value < 0 ? 0 : value;
 export const undefinedToZero = (value?: number): number => value === undefined ? 0 : value;
 
 export const reduceBySum = (list: number[]) => list.reduce((acc, curr) => acc + curr, 0);
+
+// An identity function to convery the semantic meaning of a constant, but
+// without needing the variability of TaxReturn.constants.
+export const Literal = x => x;
index 664e529c9d78fad1c89d462eec8fda9ee56146cd..5a33e48956ff0dd713dab5c502561b47edb32e30 100644 (file)
@@ -11,6 +11,8 @@ import { NotFoundError, InconsistencyError } from './Errors';
 class TestTaxReturn extends TaxReturn {
   get year() { return 2019; }
 
+  readonly constants = {};
+
   includeJointPersonForms = false;
 };
 
index ccd9ee85ffbfae5759bc63608f620bdbfa6e6e91..fea81c1c1c3c312e4d2b8d88f3908f4e0d35351a 100644 (file)
@@ -11,6 +11,8 @@ export default abstract class TaxReturn {
   private _people: Person[] = [];
   private _forms: Form[] = [];
 
+  abstract readonly constants;
+
   abstract get year(): number;
 
   abstract get includeJointPersonForms(): boolean;
index 75130cb056be9002b9205a162d88db10ad36ef40..13b5c77d2bc528c40f31efe693c98c8a564441b9 100644 (file)
@@ -9,6 +9,8 @@ import { ComputedLine, InputLine, ReferenceLine } from './Line';
 import { Edge, getLastTraceList } from './Trace';
 
 class TestTaxReturn extends TaxReturn {
+  readonly constants = undefined;
+
   get year() { return 2019; }
 
   get includeJointPersonForms() { return false; }
index edcfb110bbed11d643841add1f8dea3d3ad1f114..08299eedc7308076305c04a226c05bbccb6215cf 100644 (file)
@@ -92,13 +92,7 @@ export default class Form1040 extends Form<Form1040Input> {
         }
       }
 
-      switch (this.filingStatus) {
-        case FilingStatus.Single:
-        case FilingStatus.MarriedFilingSeparate:
-          return Math.max(deduction, 12200);
-        case FilingStatus.MarriedFilingJoint:
-          return Math.max(deduction, 24400);
-      }
+      return Math.max(deduction, tr.constants.standardDeduction[this.filingStatus]);
     }, 'Deduction'),
 
     '10': new ComputedLine((tr): number => {
@@ -136,7 +130,7 @@ export default class Form1040 extends Form<Form1040Input> {
       }
 
       // Otherwise, compute just on taxable income.
-      return computeTax(this.getValue(tr, '11b'), this.filingStatus);
+      return computeTax(this.getValue(tr, '11b'), tr);
     }, 'Tax'),
 
     '12b': new ComputedLine((tr): number => {
@@ -207,41 +201,9 @@ export default class Form1040 extends Form<Form1040Input> {
   }
 };
 
-export function computeTax(income: number, filingStatus: FilingStatus): number {
-  // From https://www.irs.gov/pub/irs-drop/rp-18-57.pdf, Section 3.01 and
-  // https://www.irs.gov/pub/irs-pdf/p17.pdf, 2019 Tax Rate Schedules (p254).
-  const taxBrackets = {
-    // Format is:
-    // [ limit-of-taxable-income, marginal-rate, base-tax ]
-    // If Income is over Row[0], pay Row[2] + (Row[1] * (Income - PreviousRow[0]))
-    [FilingStatus.MarriedFilingJoint]: [
-      [ 19400, 0.10, 0 ],
-      [ 78950, 0.12, 1940 ],
-      [ 168400, 0.22, 9086 ],
-      [ 321450, 0.24, 28765 ],
-      [ 408200, 0.32, 65497 ],
-      [ 612350, 0.35, 93257 ],
-      [ Infinity, 0.37, 164709.50 ]
-    ],
-    [FilingStatus.Single]: [
-      [ 9700, 0.10, 0 ],
-      [ 39475, 0.12, 970 ],
-      [ 84200, 0.22, 4543 ],
-      [ 160725, 0.24, 14382.50 ],
-      [ 204100, 0.32, 32748.50 ],
-      [ 510300, 0.35, 46628.50 ],
-      [ Infinity, 0.37, 153798.50 ]
-    ],
-    [FilingStatus.MarriedFilingSeparate]: [
-      [ 9700, 0.10, 0 ],
-      [ 39475, 0.12, 970 ],
-      [ 84200, 0.22, 4543 ],
-      [ 160725, 0.24, 14382.50 ],
-      [ 204100, 0.32, 32748.50 ],
-      [ 306175, 0.35, 46628.50 ],
-      [ Infinity, 0.37, 82354.75 ]
-    ]
-  }[filingStatus];
+export function computeTax(income: number, tr: TaxReturn): number {
+  const f1040 = tr.getForm(Form1040);
+  const taxBrackets = tr.constants.taxBrackets[f1040.filingStatus];
 
   let i = 0;
   while (taxBrackets[i][0] < income)
@@ -270,13 +232,8 @@ export class QDCGTaxWorksheet extends Form {
     '6': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '4') - this.getValue(tr, '5'))),
     '7': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '6'))),
     '8': new ComputedLine((tr): number => {
-      switch (tr.getForm(Form1040).filingStatus) {
-        case FilingStatus.Single:
-        case FilingStatus.MarriedFilingSeparate:
-          return 39375;
-        case FilingStatus.MarriedFilingJoint:
-          return 78750;
-      };
+      const fs = tr.getForm(Form1040).filingStatus;
+      return tr.constants.capitalGains.rate0MaxIncome[fs];
     }),
     '9': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '8'))),
     '10': new ComputedLine((tr): number => Math.min(this.getValue(tr, '7'), this.getValue(tr, '9'))),
@@ -287,11 +244,8 @@ export class QDCGTaxWorksheet extends Form {
     '13': new ReferenceLine(QDCGTaxWorksheet as any, '11'),
     '14': new ComputedLine((tr): number => this.getValue(tr, '12') - this.getValue(tr, '13')),
     '15': new ComputedLine((tr): number => {
-      switch (tr.getForm(Form1040).filingStatus) {
-        case FilingStatus.Single:                return 434550;
-        case FilingStatus.MarriedFilingSeparate: return 244425;
-        case FilingStatus.MarriedFilingJoint:    return 488850;
-      };
+      const fs = tr.getForm(Form1040).filingStatus;
+      return tr.constants.capitalGains.rate15MaxIncome[fs];
     }),
     '16': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '15'))),
     '17': new ComputedLine((tr): number => this.getValue(tr, '7') + this.getValue(tr, '11')),
@@ -306,7 +260,7 @@ export class QDCGTaxWorksheet extends Form {
       return this.getValue(tr, '22') * 0.20;
     }, 'Amount taxed at 20%'),
     '24': new ComputedLine((tr): number => {
-      return computeTax(this.getValue(tr, '7'), tr.getForm(Form1040).filingStatus);
+      return computeTax(this.getValue(tr, '7'), tr);
     }, 'Tax on line 7'),
     '25': new ComputedLine((tr): number => {
       return this.getValue(tr, '20') +
@@ -314,7 +268,7 @@ export class QDCGTaxWorksheet extends Form {
              this.getValue(tr, '24');
     }),
     '26': new ComputedLine((tr): number => {
-      return computeTax(this.getValue(tr, '1'), tr.getForm(Form1040).filingStatus);
+      return computeTax(this.getValue(tr, '1'), tr);
     }, 'Tax on line 1'),
     '27': new ComputedLine((tr): number => {
       return Math.min(this.getValue(tr, '25'), this.getValue(tr, '26'));
index 2dab2577cf87b7ff251887486bbd407313ef435f..11aa7d3a6cfd87a688aca2fe14c5d1585ce22372 100644 (file)
@@ -5,7 +5,7 @@
 
 import { Form, TaxReturn } from '../core';
 import { AccumulatorLine, ComputedLine, ReferenceLine, UnsupportedLine, sumFormLines } from '../core/Line';
-import { clampToZero } from '../core/Math';
+import { Literal, clampToZero } from '../core/Math';
 
 import Form1040, { QDCGTaxWorksheet, FilingStatus } from './Form1040';
 import Form1099INT from './Form1099INT';
@@ -56,24 +56,20 @@ export default class Form6251 extends Form {
 
     // Part II
     '5': new ComputedLine((tr): number => {
-      // [ threshold, exemption ]
-      const exemptions = {
-        [FilingStatus.Single]: [ 510300, 71700 ],
-        [FilingStatus.MarriedFilingJoint]: [ 1020600, 111700 ],
-        [FilingStatus.MarriedFilingSeparate]: [ 510300, 55850 ],
-      };
-      const exemption = exemptions[tr.getForm(Form1040).filingStatus];
+      const fs = tr.getForm(Form1040).filingStatus;
+      const exemption = tr.constants.amt.exemption[fs];
+      const phaseout = tr.constants.amt.phaseout[fs];
 
       const l4 = this.getValue(tr, '4');
-      if (l4 < exemption[0])
-        return exemption[1];
+      if (l4 < phaseout)
+        return exemption;
 
       // Exemption worksheet:
-      const wl1 = exemption[1];
+      const wl1 = exemption;
       const wl2 = l4;
-      const wl3 = exemption[0];
+      const wl3 = phaseout;
       const wl4 = clampToZero(wl2 - wl3);
-      const wl5 = wl4 * 0.25;
+      const wl5 = wl4 * Literal(0.25);
       const wl6 = clampToZero(wl1 - wl5);
       return wl6;
     }),
@@ -97,7 +93,7 @@ export default class Form6251 extends Form {
       if (part3)
         return this.getValue(tr, '40');
 
-      return computeAmtTax(f1040.filingStatus, this.getValue(tr, '6'));
+      return computeAmtTax(tr, this.getValue(tr, '6'));
     }),
     '8': new ReferenceLine(Schedule3, '1', 'Alternative minimum tax foreign tax credit'),  // Not supported - AMT FTC recalculation
     '9': new ComputedLine((tr): number => {
@@ -137,18 +133,10 @@ export default class Form6251 extends Form {
     }),
     '16': new ComputedLine((tr): number => Math.min(this.getValue(tr, '12'), this.getValue(tr, '15'))),
     '17': new ComputedLine((tr): number => this.getValue(tr, '12') - this.getValue(tr, '16')),
-    '18': new ComputedLine((tr): number => {
-      const fs = tr.getForm(Form1040).filingStatus;
-      return computeAmtTax(fs, this.getValue(tr, '17'));
-    }),
+    '18': new ComputedLine((tr): number => computeAmtTax(tr, this.getValue(tr, '17'))),
     '19': new ComputedLine((tr): number => {
-      switch (tr.getForm(Form1040).filingStatus) {
-        case FilingStatus.Single:
-        case FilingStatus.MarriedFilingSeparate:
-          return 39375;
-        case FilingStatus.MarriedFilingJoint:
-          return 78750;
-      }
+      const fs = tr.getForm(Form1040).filingStatus;
+      return tr.constants.capitalGains.rate0MaxIncome[fs];
     }),
     '20': new ComputedLine((tr): number => {
       const schedDTW = tr.findForm(ScheduleDTaxWorksheet);
@@ -163,11 +151,8 @@ export default class Form6251 extends Form {
     '23': new ComputedLine((tr): number => Math.min(this.getValue(tr, '21'), this.getValue(tr, '22'))),
     '24': new ComputedLine((tr): number => this.getValue(tr, '22') - this.getValue(tr, '23')),
     '25': new ComputedLine((tr): number => {
-      switch (tr.getForm(Form1040).filingStatus) {
-        case FilingStatus.Single: return 434550;
-        case FilingStatus.MarriedFilingSeparate: return 244425;
-        case FilingStatus.MarriedFilingJoint: return 488850;
-      }
+      const fs = tr.getForm(Form1040).filingStatus;
+      return tr.constants.capitalGains.rate15MaxIncome[fs];
     }),
     '26': new ReferenceLine(Form6251 as any, '21'),
     '27': new ComputedLine((tr): number => {
@@ -189,20 +174,17 @@ export default class Form6251 extends Form {
     '36': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '12') - this.getValue(tr, '35'))),
     '37': new ComputedLine((tr): number => this.getValue(tr, '36') * 0.25),
     '38': new ComputedLine((tr): number => sumFormLines(tr, this, ['18', '31', '34', '37'])),
-    '39': new ComputedLine((tr): number => {
-      const fs = tr.getForm(Form1040).filingStatus;
-      return computeAmtTax(fs, this.getValue(tr, '12'));
-    }),
+    '39': new ComputedLine((tr): number => computeAmtTax(tr, this.getValue(tr, '12'))),
     '40': new ComputedLine((tr): number => Math.min(this.getValue(tr, '38'), this.getValue(tr, '39'))),
   };
 };
 
-function computeAmtTax(filingStatus, amount) {
-  const mfs = filingStatus = FilingStatus.MarriedFilingSeparate;
-  const limit = mfs ? 97400 : 194800;
-  const sub = mfs ? 1948 : 3896;
+function computeAmtTax(tr: TaxReturn, amount) {
+  const fs = tr.getForm(Form1040).filingStatus;
+  const limit = tr.constants.amt.limitForRate28Percent[fs];
+  const sub = limit * 0.02;  // Difference between the two rates.
 
   if (amount < limit)
-    return amount * 0.26;
-  return (amount * 0.28) - sub;
+    return amount * Literal(0.26);
+  return (amount * Literal(0.28)) - sub;
 }
index 2c111b619164a838c82068af892987a93f608cd9..5bb5bb7a14c7df101e349d931713c3747c681c32 100644 (file)
@@ -44,7 +44,7 @@ describe('additional medicare tax', () => {
       tr.addForm(new Schedule2());
 
       expect(form.getValue(tr, '4')).toBe(300000);
-      expect(form.getValue(tr, '5')).toBe(Form8959.filingStatusLimit(filingStatus));
+      expect(form.getValue(tr, '5')).toBe(Form8959.filingStatusLimit(tr));
       expect(form.getValue(tr, '6')).toBe(filingStatusToResults[filingStatus]['6']);
       expect(form.getValue(tr, '18')).toBeCloseTo(form.getValue(tr, '6') * 0.009);
 
@@ -80,7 +80,7 @@ describe('no additional medicare tax', () => {
       tr.addForm(new Schedule2());
 
       expect(form.getValue(tr, '4')).toBe(110000);
-      expect(form.getValue(tr, '5')).toBe(Form8959.filingStatusLimit(filingStatus));
+      expect(form.getValue(tr, '5')).toBe(Form8959.filingStatusLimit(tr));
       expect(form.getValue(tr, '6')).toBe(0);
       expect(form.getValue(tr, '18')).toBe(0);
 
index 0aef950b362c0b7078df3b0d571612b81bfe43ad..ceb505adbaad345a037cd6106769a2771c8923d3 100644 (file)
@@ -21,13 +21,13 @@ export default class Form8959 extends Form {
       return sumFormLines(tr, this, ['1', '2', '3']);
     }),
     '5': new ComputedLine((tr): number => {
-      return Form8959.filingStatusLimit(tr.getForm(Form1040).filingStatus);
+      return tr.constants.medicare.additionalWithholdingLimit[tr.getForm(Form1040).filingStatus];
     }),
     '6': new ComputedLine((tr): number => {
       return clampToZero(this.getValue(tr, '4') - this.getValue(tr, '5'));
     }),
     '7': new ComputedLine((tr): number => {
-      return this.getValue(tr, '6') * 0.009;
+      return this.getValue(tr, '6') * tr.constants.medicare.additionalWithholdingRate;
     }, 'Additional Medicare tax on Medicare wages'),
 
     // All of Section 2 and 3 skipped.
@@ -40,7 +40,7 @@ export default class Form8959 extends Form {
     '19': new AccumulatorLine(W2, '6', 'Medicare tax withheld'),
     '20': new ReferenceLine(Form8959 as any, '1'),
     '21': new ComputedLine((tr): number => {
-      return this.getValue(tr, '20') * 0.0145;
+      return this.getValue(tr, '20') * tr.constants.medicare.withholdingRate;
     }, 'Regular Medicare withholding on Medicare wages'),
     '22': new ComputedLine((tr): number => {
       return clampToZero(this.getValue(tr, '19') - this.getValue(tr, '21'));
@@ -51,11 +51,8 @@ export default class Form8959 extends Form {
     }),
   };
 
-  static filingStatusLimit(filingStatus: FilingStatus): number {
-    switch (filingStatus) {
-      case FilingStatus.Single:                return 200000;
-      case FilingStatus.MarriedFilingJoint:    return 250000;
-      case FilingStatus.MarriedFilingSeparate: return 125000;
-    }
+  static filingStatusLimit(tr: TaxReturn): number {
+    const filingStatus = tr.getForm(Form1040).filingStatus;
+    return tr.constants.medicare.additionalWithholdingLimit[filingStatus];
   }
 };
index d45ac4ef48273f5d2d3989f6cceb2b4ea491205f..146a2bbbae085dcda19004c193f6f5cf67b1fdac 100644 (file)
@@ -76,7 +76,7 @@ describe('net investment income tax', () => {
       expect(f.getValue(tr, '11')).toBe(0);
       expect(f.getValue(tr, '12')).toBe(5555);
       expect(f.getValue(tr, '13')).toBe(305555);
-      expect(f.getValue(tr, '14')).toBe(Form8960.filingStatusLimit(filingStatus));
+      expect(f.getValue(tr, '14')).toBe(Form8960.filingStatusLimit(tr));
       expect(f.getValue(tr, '15')).toBe(filingStatusToResult[filingStatus]);
       expect(f.getValue(tr, '16')).toBe(5555);
       expect(f.getValue(tr, '17')).toBe(5555 * 0.038);
index 359138998cb0e0a7077df1f3f1677ec257fa3f2a..c6be3203ad2b1ae48b688208372f922dc0cae3e8 100644 (file)
@@ -7,6 +7,7 @@ import { Form, TaxReturn } from '../core';
 import { ComputedLine, ReferenceLine, UnsupportedLine, sumFormLines } from '../core/Line';
 import { clampToZero, undefinedToZero } from '../core/Math';
 
+import { Constants } from './TaxReturn';
 import Form1040, { FilingStatus } from './Form1040';
 import Schedule1 from './Schedule1';
 
@@ -49,20 +50,17 @@ export default class Form8960 extends Form {
     '12': new ComputedLine((tr): number => this.getValue(tr, '8') - this.getValue(tr, '11'), 'Net investment income'),
     '13': new ReferenceLine(Form1040, '8b', 'Modified adjusted gross income'),
     '14': new ComputedLine((tr): number => {
-      return Form8960.filingStatusLimit(tr.getForm(Form1040).filingStatus);
+      return Form8960.filingStatusLimit(tr);
     }, 'Threshold'),
     '15': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '13') - this.getValue(tr, '14'))),
     '16': new ComputedLine((tr): number => Math.min(this.getValue(tr, '12'), this.getValue(tr, '15'))),
-    '17': new ComputedLine((tr): number => this.getValue(tr, '16') * 0.038, 'Net investment income tax'),
+    '17': new ComputedLine((tr): number => this.getValue(tr, '16') * tr.constants.niit.rate, 'Net investment income tax'),
 
     // 18 - 21 not supported (Estates and Trusts)
   };
 
-  static filingStatusLimit(filingStatus: FilingStatus): number {
-    switch (filingStatus) {
-      case FilingStatus.MarriedFilingJoint:    return 250000;
-      case FilingStatus.MarriedFilingSeparate: return 125000;
-      case FilingStatus.Single:                return 200000;
-    }
+  static filingStatusLimit(tr: TaxReturn): number {
+    const filingStatus = tr.getForm(Form1040).filingStatus;
+    return tr.constants.niit.limit[filingStatus];
   }
 };
index 1590fb7d7d36cef298d969a64496a55fd5c467da..fbbc2d2140f7f43b0f47ea574f9830c6c747866f 100644 (file)
@@ -5,7 +5,7 @@
 
 import { Form, Person, TaxReturn } from '../core';
 import { AccumulatorLine, ComputedLine, InputLine, UnsupportedLine } from '../core/Line';
-import { clampToZero } from '../core/Math';
+import { Literal, clampToZero } from '../core/Math';
 
 import Form1040 from './Form1040';
 import Form1099DIV from './Form1099DIV';
@@ -28,7 +28,7 @@ export default class Form8995REIT extends Form {
     '28': new AccumulatorLine(Form1099DIV, '5', 'Qualified REIT dividends'),
     '29': new InputLine<Form8995REITInput>('qualifiedReitDividendCarryforward', undefined, 0),
     '30': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '28') + this.getValue(tr, '29'))),
-    '31': new ComputedLine((tr): number => this.getValue(tr, '30') * 0.20, 'REIT and PTP component'),
+    '31': new ComputedLine((tr): number => this.getValue(tr, '30') * Literal(0.20), 'REIT and PTP component'),
     '32': new ComputedLine((tr): number => this.getValue(tr, '27') + this.getValue(tr, '31'), 'QBI deduction before limitation'),
     '33': new ComputedLine((tr): number => {
       const f1040 = tr.getForm(Form1040);
@@ -51,7 +51,7 @@ export default class Form8995REIT extends Form {
       return value;
     }, 'Net capital gain'),
     '35': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '33') - this.getValue(tr, '34'))),
-    '36': new ComputedLine((tr): number => this.getValue(tr, '35') * 0.20, 'Income limitation'),
+    '36': new ComputedLine((tr): number => this.getValue(tr, '35') * Literal(0.20), 'Income limitation'),
     '37': new ComputedLine((tr): number => Math.min(this.getValue(tr, '32'), this.getValue(tr, '36'))),
     '38': new UnsupportedLine('DPAD under section 199A(g) allocated from an agricultural or horticultural cooperative'),
     '39': new ComputedLine((tr): number => this.getValue(tr, '37') + this.getValue(tr, '38')),
index a7a4fe46d18e634b6f4be39061c428037c36bffc..deaa2830b1c095bc82fc55832e9eb1226dbc5b15 100644 (file)
@@ -179,13 +179,8 @@ export class SALTWorksheet extends Form<SALTWorksheetInput> {
     }),
     '4': new InputLine<SALTWorksheetInput>('prevYearItemizedDeductions'),
     '5': new ComputedLine((tr): number => {
-      switch (this.getInput('prevYearFilingStatus')) {
-        case FilingStatus.Single:
-        case FilingStatus.MarriedFilingSeparate:
-          return 12000;
-        case FilingStatus.MarriedFilingJoint:
-          return 24000;
-      }
+      const fs = this.getInput('prevYearFilingStatus');
+      return tr.constants.prevYearStandardDeduction[fs];
     }, 'Previous year standard deduction'),
     '6': new ComputedLine((tr): number => 0, 'Special situations'),  // Not supported
     '7': new ComputedLine((tr): number => this.getValue(tr, '5') + this.getValue(tr, '6')),
index 20e42a45551e481299a6aa88a3295e0cde7df9f1..d7ff1b807a2ffbf3cfd68c787a6e7da005140f4c 100644 (file)
@@ -41,12 +41,12 @@ export default class Schedule2 extends Form {
       let value = 0;
 
       // Additional medicare tax.
-      if (wages > Form8959.filingStatusLimit(filingStatus)) {
+      if (wages > Form8959.filingStatusLimit(tr)) {
         value += tr.getForm(Form8959).getValue(tr, '18');
       }
 
       // Net investment income tax.
-      if (wages > Form8960.filingStatusLimit(filingStatus) &&
+      if (wages > Form8960.filingStatusLimit(tr) &&
           (tr.findForms(Form1099DIV).length || tr.findForms(Form1099INT).length)) {
         value += tr.getForm(Form8960).getValue(tr, '17');
       }
index 776f1bae7cbc59588a2d44da4178fbbc25b66f58..c8e01cf40818b5ed364f361b6e45a6bf8f400e41 100644 (file)
@@ -27,7 +27,7 @@ export default class Schedule3 extends Form<Schedule3Input> {
 
       const totalForeignTax = (new AccumulatorLine(Form1099DIV, '7')).value(tr) +
                               (new AccumulatorLine(Form1099INT, '6')).value(tr);
-      const limit = f1040.filingStatus == FilingStatus.MarriedFilingJoint ? 600 : 300;
+      const limit = tr.constants.foreignTaxCreditWithoutForm1116Limit[f1040.filingStatus];
 
       if (totalForeignTax < limit) {
         const sched2l2 = new ReferenceLine(Schedule2, '2', undefined, 0);
index acb49cdea13842f266ce159da9ba5b1d99f8cadf..af8946f38152238238a8f5e5d238774b64b7bf03 100644 (file)
@@ -41,7 +41,7 @@ export default class ScheduleA extends Form<ScheduleAInput> {
     // Medical and dental expenses
     '1': new Input('medicalAndDentalExpenses', 'Medical and dental expenses', 0),
     '2': new ReferenceLine(Form1040, '8b'),
-    '3': new ComputedLine((tr): number => this.getValue(tr, '2') * 0.075),
+    '3': new ComputedLine((tr): number => this.getValue(tr, '2') * tr.constants.medicalDeductionLimitationPercent),
     '4': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '3'))),
 
     // Taxes you paid
@@ -51,7 +51,7 @@ export default class ScheduleA extends Form<ScheduleAInput> {
     '5d': new ComputedLine((tr): number => sumFormLines(tr, this, ['5a', '5b', '5c'])),
     '5e': new ComputedLine((tr): number => {
       const fs = tr.getForm(Form1040).filingStatus;
-      const limit = fs == FilingStatus.MarriedFilingSeparate ? 5000 : 10000;
+      const limit = tr.constants.saltLimit[fs];
       return Math.min(this.getValue(tr, '5d'), limit);
     }),
     '6': new Input('otherTaxes', 'Other taxes', 0),
index e46f21bb3c340e55c6dabac9ad7b851f0795e8e9..425608bd7221cdd214fcf3fa70613d42f19580cd 100644 (file)
@@ -5,7 +5,7 @@
 
 import { Form, Person, TaxReturn } from '../core';
 import { Line, AccumulatorLine, ComputedLine, ReferenceLine, UnsupportedLine, sumFormLines, sumLineOfForms } from '../core/Line';
-import { clampToZero } from '../core/Math';
+import { Literal, clampToZero } from '../core/Math';
 import { NotFoundError, UnsupportedFeatureError } from '../core/Errors';
 
 import Form8949, { Form8949Box } from './Form8949';
@@ -89,8 +89,8 @@ export class ScheduleDTaxWorksheet extends Form {
   readonly name = 'Schedule D Tax Worksheet';
 
   readonly lines = {
-    '1': new ReferenceLine(Form1040, '11b'),
-    '2': new ReferenceLine(Form1040, '3a'),
+    '1': new ReferenceLine(Form1040, '11b', 'Taxable income'),
+    '2': new ReferenceLine(Form1040, '3a', 'Qualified dividends'),
     '3': new UnsupportedLine('Form 4952@4g'),
     '4': new UnsupportedLine('Form 4952@4e'),
     '5': new ComputedLine((tr): number => 0),
@@ -98,7 +98,7 @@ export class ScheduleDTaxWorksheet extends Form {
     '7': new ComputedLine((tr): number => {
       const schedD = tr.getForm(ScheduleD);
       return Math.min(schedD.getValue(tr, '15'), schedD.getValue(tr, '16'));
-    }),
+    }, 'Capital loss'),
     '8': new ComputedLine((tr): number => {
       return Math.min(this.getValue(tr, '3'), this.getValue(tr, '4'));
     }),
@@ -107,59 +107,40 @@ export class ScheduleDTaxWorksheet extends Form {
     '11': new ComputedLine((tr): number => {
       const schedD = tr.getForm(ScheduleD);
       return schedD.getValue(tr, '18') + schedD.getValue(tr, '19');
-    }),
+    }, '28% gains and unrecaptured gains'),
     '12': new ComputedLine((tr): number => Math.min(this.getValue(tr, '9'), this.getValue(tr, '11'))),
     '13': new ComputedLine((tr): number => this.getValue(tr, '10') - this.getValue(tr, '12')),
     '14': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '13'))),
     '15': new ComputedLine((tr): number => {
-      switch (tr.getForm(Form1040).filingStatus) {
-        case FilingStatus.Single:
-        case FilingStatus.MarriedFilingSeparate:
-          return 39375;
-        case FilingStatus.MarriedFilingJoint:
-          return 78750;
-      }
+      const fs = tr.getForm(Form1040).filingStatus;
+      return tr.constants.capitalGains.rate0MaxIncome[fs];
     }),
     '16': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '15'))),
     '17': new ComputedLine((tr): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '16'))),
     '18': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '10'))),
     '19': new ComputedLine((tr): number => {
-      let threshold: number;
-      switch (tr.getForm(Form1040).filingStatus) {
-        case FilingStatus.Single:
-        case FilingStatus.MarriedFilingSeparate:
-          threshold = 160725;
-          break;
-        case FilingStatus.MarriedFilingJoint:
-          threshold = 321450;
-          break;
-      }
+      const fs = tr.getForm(Form1040).filingStatus;
+      const threshold = tr.constants.qualifiedBusinessIncomeDeductionThreshold[fs];
       return Math.min(this.getValue(tr, '1'), threshold);
     }),
     '20': new ComputedLine((tr): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '19'))),
     '21': new ComputedLine((tr): number => Math.max(this.getValue(tr, '18'), this.getValue(tr, '20'))),
-    '22': new ComputedLine((tr): number => this.getValue(tr, '16') - this.getValue(tr, '17')),
+    '22': new ComputedLine((tr): number => this.getValue(tr, '16') - this.getValue(tr, '17'), 'Amount taxed at 0%'),
     '23': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '13'))),
     '24': new ReferenceLine(ScheduleDTaxWorksheet as any, '22'),
     '25': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '23') - this.getValue(tr, '24'))),
     '26': new ComputedLine((tr): number => {
-      switch (tr.getForm(Form1040).filingStatus) {
-        case FilingStatus.Single:
-          return 434550;
-        case FilingStatus.MarriedFilingSeparate:
-          return 244425;
-        case FilingStatus.MarriedFilingJoint:
-          return 488850;
-      }
+      const fs = tr.getForm(Form1040).filingStatus;
+      return tr.constants.capitalGains.rate15MaxIncome[fs];
     }),
     '27': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '26'))),
     '28': new ComputedLine((tr): number => this.getValue(tr, '21') + this.getValue(tr, '22')),
     '29': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '27') - this.getValue(tr, '28'))),
-    '30': new ComputedLine((tr): number => Math.min(this.getValue(tr, '25'), this.getValue(tr, '29'))),
-    '31': new ComputedLine((tr): number => this.getValue(tr, '30') * 0.15),
+    '30': new ComputedLine((tr): number => Math.min(this.getValue(tr, '25'), this.getValue(tr, '29')), 'Amount taxed at 15%'),
+    '31': new ComputedLine((tr): number => this.getValue(tr, '30') * Literal(0.15), '15% Tax'),
     '32': new ComputedLine((tr): number => this.getValue(tr, '24') + this.getValue(tr, '30')),
-    '33': new ComputedLine((tr): number => this.getValue(tr, '23') - this.getValue(tr, '32')),
-    '34': new ComputedLine((tr): number => this.getValue(tr, '33') * 0.20),
+    '33': new ComputedLine((tr): number => this.getValue(tr, '23') - this.getValue(tr, '32'), 'Amount taxed at 20%'),
+    '34': new ComputedLine((tr): number => this.getValue(tr, '33') * Literal(0.20), '20% Tax'),
     '35': new ComputedLine((tr): number => {
       const schedD = tr.getForm(ScheduleD);
       return Math.min(this.getValue(tr, '9'), schedD.getValue(tr, '19'));
@@ -168,7 +149,7 @@ export class ScheduleDTaxWorksheet extends Form {
     '37': new ReferenceLine(ScheduleDTaxWorksheet as any, '1'),
     '38': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '36') - this.getValue(tr, '37'))),
     '39': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '35') - this.getValue(tr, '38'))),
-    '40': new ComputedLine((tr): number => this.getValue(tr, '39') * 0.25),
+    '40': new ComputedLine((tr): number => this.getValue(tr, '39') * Literal(0.25), 'Tax on unrecaptured gains'),
     '41': new ComputedLine((tr): number => {
       const schedD = tr.getForm(ScheduleD);
       if (schedD.getValue(tr, '18'))
@@ -183,19 +164,19 @@ export class ScheduleDTaxWorksheet extends Form {
     '43': new ComputedLine((tr): number => {
       if (!tr.getForm(ScheduleD).getValue(tr, '18'))
         return 0;
-      return this.getValue(tr, '42') * 0.28;
-    }),
+      return this.getValue(tr, '42') * Literal(0.28);
+    }, '28% gain tax'),
     '44': new ComputedLine((tr): number => {
       const income = this.getValue(tr, '21');
-      return computeTax(income, tr.getForm(Form1040).filingStatus);
-    }),
+      return computeTax(income, tr);
+    }, 'Nominal rate tax'),
     '45': new ComputedLine((tr): number => {
       return sumFormLines(tr, this, ['31', '34', '40', '43', '44']);
-    }),
+    }, 'Schedule D tax'),
     '46': new ComputedLine((tr): number => {
       const income = this.getValue(tr, '1');
-      return computeTax(income, tr.getForm(Form1040).filingStatus);
-    }),
-    '47': new ComputedLine((tr): number => Math.min(this.getValue(tr, '45'), this.getValue(tr, '46'))),
+      return computeTax(income, tr);
+    }, 'Income tax'),
+    '47': new ComputedLine((tr): number => Math.min(this.getValue(tr, '45'), this.getValue(tr, '46')), 'Tax on all taxable income'),
   };
 };
index 99741a1c21e384141782d8802cc3a81f44cade62..8716449fcda8582af061f73fb56bd37169a9b5a4 100644 (file)
@@ -7,7 +7,127 @@ import { TaxReturn as BaseTaxReturn } from '../core';
 
 import Form1040, { FilingStatus } from './Form1040';
 
+export const Constants = {
+  taxBrackets: {
+    // From https://www.irs.gov/pub/irs-drop/rp-18-57.pdf, Section 3.01 and
+    // https://www.irs.gov/pub/irs-pdf/p17.pdf, 2019 Tax Rate Schedules (p254).
+    // Format is: [ limit-of-taxable-income, marginal-rate, base-tax ]
+    // If Income is over Row[0], pay Row[2] + (Row[1] * (Income - PreviousRow[0]))
+    [FilingStatus.MarriedFilingJoint]: [
+      [ 19400, 0.10, 0 ],
+      [ 78950, 0.12, 1940 ],
+      [ 168400, 0.22, 9086 ],
+      [ 321450, 0.24, 28765 ],
+      [ 408200, 0.32, 65497 ],
+      [ 612350, 0.35, 93257 ],
+      [ Infinity, 0.37, 164709.50 ]
+    ],
+    [FilingStatus.Single]: [
+      [ 9700, 0.10, 0 ],
+      [ 39475, 0.12, 970 ],
+      [ 84200, 0.22, 4543 ],
+      [ 160725, 0.24, 14382.50 ],
+      [ 204100, 0.32, 32748.50 ],
+      [ 510300, 0.35, 46628.50 ],
+      [ Infinity, 0.37, 153798.50 ]
+    ],
+    [FilingStatus.MarriedFilingSeparate]: [
+      [ 9700, 0.10, 0 ],
+      [ 39475, 0.12, 970 ],
+      [ 84200, 0.22, 4543 ],
+      [ 160725, 0.24, 14382.50 ],
+      [ 204100, 0.32, 32748.50 ],
+      [ 306175, 0.35, 46628.50 ],
+      [ Infinity, 0.37, 82354.75 ]
+    ]
+  },
+
+  standardDeduction: {
+    [FilingStatus.MarriedFilingJoint]: 24400,
+    [FilingStatus.Single]: 12200,
+    [FilingStatus.MarriedFilingSeparate]: 12200,
+  },
+
+  niit: {
+    rate: 0.038,
+    limit: {
+      [FilingStatus.MarriedFilingJoint]: 250000,
+      [FilingStatus.MarriedFilingSeparate]: 125000,
+      [FilingStatus.Single]: 200000,
+    },
+  },
+
+  medicare: {
+    withholdingRate: 0.0145,
+    additionalWithholdingRate: 0.009,
+    additionalWithholdingLimit: {
+      [FilingStatus.Single]: 200000,
+      [FilingStatus.MarriedFilingJoint]: 250000,
+      [FilingStatus.MarriedFilingSeparate]: 125000,
+    },
+  },
+
+  capitalGains: {
+    rate0MaxIncome: {
+      [FilingStatus.MarriedFilingJoint]: 78750,
+      [FilingStatus.Single]: 39375,
+      [FilingStatus.MarriedFilingSeparate]: 39375,
+    },
+    rate15MaxIncome: {
+      [FilingStatus.MarriedFilingJoint]: 488850,
+      [FilingStatus.MarriedFilingSeparate]: 244425,
+      [FilingStatus.Single]: 434550,
+    },
+  },
+
+  qualifiedBusinessIncomeDeductionThreshold: {
+    [FilingStatus.MarriedFilingJoint]: 321450, // RP-18-57, Section 3.27 indicates this should be 321400, but it does not match the 24% tax bracket nor Sched D Tax Worksheet line 19.
+    [FilingStatus.MarriedFilingSeparate]: 160725,
+    [FilingStatus.Single]: 160725,
+  },
+
+  foreignTaxCreditWithoutForm1116Limit: {
+    [FilingStatus.MarriedFilingJoint]: 600,
+    [FilingStatus.MarriedFilingSeparate]: 300,
+    [FilingStatus.Single]: 300,
+  },
+
+  saltLimit: {
+    [FilingStatus.MarriedFilingJoint]: 10000,
+    [FilingStatus.Single]: 10000,
+    [FilingStatus.MarriedFilingSeparate]: 5000,
+  },
+
+  medicalDeductionLimitationPercent: 0.075,
+
+  prevYearStandardDeduction: {
+    [FilingStatus.MarriedFilingJoint]: 24000,
+    [FilingStatus.Single]: 12000,
+    [FilingStatus.MarriedFilingSeparate]: 12000,
+  },
+
+  amt: {
+    exemption: {
+      [FilingStatus.MarriedFilingJoint]: 111700,
+      [FilingStatus.Single]: 71700,
+      [FilingStatus.MarriedFilingSeparate]: 55850,
+    },
+    phaseout: {
+      [FilingStatus.MarriedFilingJoint]: 1020600,
+      [FilingStatus.Single]: 510300,
+      [FilingStatus.MarriedFilingSeparate]: 510300,
+    },
+    limitForRate28Percent: {
+      [FilingStatus.MarriedFilingJoint]: 194800,
+      [FilingStatus.Single]: 194800,
+      [FilingStatus.MarriedFilingSeparate]: 97400,
+    },
+  },
+};
+
 export default class TaxReturn extends BaseTaxReturn {
+  readonly constants = Constants;
+
   get year() { return 2019; }
 
   get includeJointPersonForms() {