From 9c54a12c64eafd94dad707630ad471dfaf9977f2 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 2 Aug 2020 12:07:09 -0400 Subject: [PATCH] Extract constant values from fed2019 Forms as named definitions. 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. --- src/core/Form.test.ts | 1 + src/core/Line.test.ts | 1 + src/core/Math.ts | 4 ++ src/core/TaxReturn.test.ts | 2 + src/core/TaxReturn.ts | 2 + src/core/Trace.test.ts | 2 + src/fed2019/Form1040.ts | 68 ++++---------------- src/fed2019/Form6251.ts | 62 +++++++----------- src/fed2019/Form8959.test.ts | 4 +- src/fed2019/Form8959.ts | 15 ++--- src/fed2019/Form8960.test.ts | 2 +- src/fed2019/Form8960.ts | 14 ++-- src/fed2019/Form8995.ts | 6 +- src/fed2019/Schedule1.ts | 9 +-- src/fed2019/Schedule2.ts | 4 +- src/fed2019/Schedule3.ts | 2 +- src/fed2019/ScheduleA.ts | 4 +- src/fed2019/ScheduleD.ts | 69 ++++++++------------ src/fed2019/TaxReturn.ts | 120 +++++++++++++++++++++++++++++++++++ 19 files changed, 215 insertions(+), 176 deletions(-) diff --git a/src/core/Form.test.ts b/src/core/Form.test.ts index 5b00536..9021d60 100644 --- a/src/core/Form.test.ts +++ b/src/core/Form.test.ts @@ -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; } }; diff --git a/src/core/Line.test.ts b/src/core/Line.test.ts index 4a59638..df960c8 100644 --- a/src/core/Line.test.ts +++ b/src/core/Line.test.ts @@ -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; } }; diff --git a/src/core/Math.ts b/src/core/Math.ts index dd921d5..ce14b7e 100644 --- a/src/core/Math.ts +++ b/src/core/Math.ts @@ -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; diff --git a/src/core/TaxReturn.test.ts b/src/core/TaxReturn.test.ts index 664e529..5a33e48 100644 --- a/src/core/TaxReturn.test.ts +++ b/src/core/TaxReturn.test.ts @@ -11,6 +11,8 @@ import { NotFoundError, InconsistencyError } from './Errors'; class TestTaxReturn extends TaxReturn { get year() { return 2019; } + readonly constants = {}; + includeJointPersonForms = false; }; diff --git a/src/core/TaxReturn.ts b/src/core/TaxReturn.ts index ccd9ee8..fea81c1 100644 --- a/src/core/TaxReturn.ts +++ b/src/core/TaxReturn.ts @@ -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; diff --git a/src/core/Trace.test.ts b/src/core/Trace.test.ts index 75130cb..13b5c77 100644 --- a/src/core/Trace.test.ts +++ b/src/core/Trace.test.ts @@ -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; } diff --git a/src/fed2019/Form1040.ts b/src/fed2019/Form1040.ts index edcfb11..08299ee 100644 --- a/src/fed2019/Form1040.ts +++ b/src/fed2019/Form1040.ts @@ -92,13 +92,7 @@ export default class Form1040 extends Form { } } - 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 { } // 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 { } }; -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')); diff --git a/src/fed2019/Form6251.ts b/src/fed2019/Form6251.ts index 2dab257..11aa7d3 100644 --- a/src/fed2019/Form6251.ts +++ b/src/fed2019/Form6251.ts @@ -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; } diff --git a/src/fed2019/Form8959.test.ts b/src/fed2019/Form8959.test.ts index 2c111b6..5bb5bb7 100644 --- a/src/fed2019/Form8959.test.ts +++ b/src/fed2019/Form8959.test.ts @@ -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); diff --git a/src/fed2019/Form8959.ts b/src/fed2019/Form8959.ts index 0aef950..ceb505a 100644 --- a/src/fed2019/Form8959.ts +++ b/src/fed2019/Form8959.ts @@ -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]; } }; diff --git a/src/fed2019/Form8960.test.ts b/src/fed2019/Form8960.test.ts index d45ac4e..146a2bb 100644 --- a/src/fed2019/Form8960.test.ts +++ b/src/fed2019/Form8960.test.ts @@ -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); diff --git a/src/fed2019/Form8960.ts b/src/fed2019/Form8960.ts index 3591389..c6be320 100644 --- a/src/fed2019/Form8960.ts +++ b/src/fed2019/Form8960.ts @@ -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]; } }; diff --git a/src/fed2019/Form8995.ts b/src/fed2019/Form8995.ts index 1590fb7..fbbc2d2 100644 --- a/src/fed2019/Form8995.ts +++ b/src/fed2019/Form8995.ts @@ -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('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')), diff --git a/src/fed2019/Schedule1.ts b/src/fed2019/Schedule1.ts index a7a4fe4..deaa283 100644 --- a/src/fed2019/Schedule1.ts +++ b/src/fed2019/Schedule1.ts @@ -179,13 +179,8 @@ export class SALTWorksheet extends Form { }), '4': new InputLine('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')), diff --git a/src/fed2019/Schedule2.ts b/src/fed2019/Schedule2.ts index 20e42a4..d7ff1b8 100644 --- a/src/fed2019/Schedule2.ts +++ b/src/fed2019/Schedule2.ts @@ -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'); } diff --git a/src/fed2019/Schedule3.ts b/src/fed2019/Schedule3.ts index 776f1ba..c8e01cf 100644 --- a/src/fed2019/Schedule3.ts +++ b/src/fed2019/Schedule3.ts @@ -27,7 +27,7 @@ export default class Schedule3 extends Form { 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); diff --git a/src/fed2019/ScheduleA.ts b/src/fed2019/ScheduleA.ts index acb49cd..af8946f 100644 --- a/src/fed2019/ScheduleA.ts +++ b/src/fed2019/ScheduleA.ts @@ -41,7 +41,7 @@ export default class ScheduleA extends Form { // 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 { '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), diff --git a/src/fed2019/ScheduleD.ts b/src/fed2019/ScheduleD.ts index e46f21b..425608b 100644 --- a/src/fed2019/ScheduleD.ts +++ b/src/fed2019/ScheduleD.ts @@ -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'), }; }; diff --git a/src/fed2019/TaxReturn.ts b/src/fed2019/TaxReturn.ts index 99741a1..8716449 100644 --- a/src/fed2019/TaxReturn.ts +++ b/src/fed2019/TaxReturn.ts @@ -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() { -- 2.22.5