From 777f3f871e5acc4f0c83e5b00960e095fc8d54bf Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 31 Jan 2021 01:23:05 -0500 Subject: [PATCH] Move some computations for Form1040 into methods. Other forms can reference the values by symbolic name rather than line number using the new SymbolicLine. Thus when Form 1040 changes with minor line shifts, as it does in 2020, all the dependent forms do not need to be updated. --- src/core/Line.ts | 23 ++++++ src/fed2019/Form1040.ts | 156 ++++++++++++++++++++--------------- src/fed2019/Form1116.ts | 10 +-- src/fed2019/Form6251.ts | 14 ++-- src/fed2019/Form8960.ts | 8 +- src/fed2019/Form8995.ts | 6 +- src/fed2019/Schedule1.ts | 2 +- src/fed2019/Schedule3.ts | 2 +- src/fed2019/ScheduleA.ts | 4 +- src/fed2019/ScheduleD.ts | 8 +- src/fed2020/Form1040.test.ts | 1 + 11 files changed, 139 insertions(+), 95 deletions(-) diff --git a/src/core/Line.ts b/src/core/Line.ts index 821483f..bdd5571 100644 --- a/src/core/Line.ts +++ b/src/core/Line.ts @@ -77,6 +77,29 @@ export class ReferenceLine this.K(tr)); +export class SymbolicLine> }, + K extends keyof F> + extends Line> { + private _form: FormClass; + private _key: K; + + constructor(form: FormClass, key: K, description?: string) { + super(description || `Reference ${form.name}/${key}`); + this._form = form; + this._key = key; + } + + value(tr: TaxReturn): ReturnType { + Trace.begin(this); + const form: F = tr.findForm(this._form); + const value = form[this._key](tr); + Trace.end(); + return value; + } +} + export class InputLine extends Line { private _input: T; private _fallback: U[T]; diff --git a/src/fed2019/Form1040.ts b/src/fed2019/Form1040.ts index 08299ee..c9e8c06 100644 --- a/src/fed2019/Form1040.ts +++ b/src/fed2019/Form1040.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-only import { Form, TaxReturn } from '../core'; -import { Line, AccumulatorLine, ComputedLine, ReferenceLine, UnsupportedLine, sumFormLines, sumLineOfForms } from '../core/Line'; +import { Line, AccumulatorLine, ComputedLine, ReferenceLine, SymbolicLine, UnsupportedLine, sumFormLines, sumLineOfForms } from '../core/Line'; import { UnsupportedFeatureError } from '../core/Errors'; import { clampToZero, reduceBySum, undefinedToZero } from '../core/Math'; @@ -34,6 +34,83 @@ export interface Form1040Input { export default class Form1040 extends Form { readonly name = '1040'; + taxableInterest(tr: TaxReturn): number { + const value = (new AccumulatorLine(Form1099INT, '1')).value(tr) + + (new AccumulatorLine(Form1099INT, '3')).value(tr); + return value; + } + + qualifiedDividends(tr: TaxReturn): number { + return this.getValue(tr, '3a'); + } + + adjustedGrossIncome(tr: TaxReturn): number { + return this.getValue(tr, '7b') - this.getValue(tr, '8a'); + } + + deduction(tr: TaxReturn): number { + let deduction = 0; + const schedA = tr.findForm(ScheduleA); + if (schedA) { + deduction = schedA.getValue(tr, '17'); + if (schedA.getValue(tr, '18')) { + return deduction; + } + } + + return Math.max(deduction, tr.constants.standardDeduction[this.filingStatus]); + } + + qualifiedBusinessIncomeDeduction(tr: TaxReturn): number { + const f8995 = tr.findForm(Form8995REIT); + if (f8995) + return f8995.getValue(tr, '39'); + return 0; + } + + capitalGainOrLoss(tr: TaxReturn): number { + const schedD = tr.findForm(ScheduleD); + if (!schedD) + return 0; + + const l6 = schedD.getValue(tr, '16'); + if (l6 >= 0) + return l6; + return schedD.getValue(tr, '21'); + } + + totalIncome(tr: TaxReturn): number { + return sumFormLines(tr, this, ['1', '2b', '3b', '4b', '4d', '5b', '6', '7a']); + } + + taxableIncome(tr: TaxReturn): number { + return clampToZero(this.getValue(tr, '8b') - this.getValue(tr, '11a')); + } + + tax(tr: TaxReturn): number { + // Not supported: + // Form 8814 (election to report child's interest or dividends) + // Form 4972 (relating to lump-sum distributions) + + const schedD = tr.findForm(ScheduleD); + // Line 18 and 19 are not undefined or 0. + if (schedD && !schedD.getValue(tr, '20')) { + // Use ScheD tax worksheet; + const schedDtw = tr.findForm(ScheduleDTaxWorksheet); + if (schedDtw) + return schedDtw.getValue(tr, '47'); + } + + // If there are qualified dividends, use the QDCGTW. + if (this.qualifiedDividends(tr) > 0) { + const qdcgtw = tr.getForm(QDCGTaxWorksheet); + return qdcgtw.getValue(tr, '27'); + } + + // Otherwise, compute just on taxable income. + return computeTax(this.taxableIncome(tr), tr); + } + readonly lines = { '1': new AccumulatorLine(W2, '1', 'Wages, salaries, tips, etc.'), '2a': new ComputedLine((tr): number => { @@ -41,11 +118,7 @@ export default class Form1040 extends Form { (new AccumulatorLine(Form1099DIV, '11')).value(tr); return value; }, 'Tax-exempt interest'), - '2b': new ComputedLine((tr): number => { - const value = (new AccumulatorLine(Form1099INT, '1')).value(tr) + - (new AccumulatorLine(Form1099INT, '3')).value(tr); - return value; - }, 'Taxable interest'), + '2b': new ComputedLine((tr) => this.taxableInterest(tr), 'Taxable interest'), '3a': new AccumulatorLine(Form1099DIV, '1b', 'Qualified dividends'), '3b': new AccumulatorLine(Form1099DIV, '1a', 'Ordinary dividends'), '4a': new ComputedLine((tr): number => { @@ -60,78 +133,25 @@ export default class Form1040 extends Form { '4d': new UnsupportedLine('Pensions and annuities, taxable amount'), '5a': new UnsupportedLine('Social security benefits'), '5b': new UnsupportedLine('Social security benefits, taxable amount'), - '6': new ComputedLine((tr): number => { - const schedD = tr.findForm(ScheduleD); - if (!schedD) - return 0; - - const l6 = schedD.getValue(tr, '16'); - if (l6 >= 0) - return l6; - return schedD.getValue(tr, '21'); - }, 'Capital gain/loss'), + '6': new ComputedLine((tr) => this.capitalGainOrLoss(tr), 'Capital gain/loss'), '7a': new ReferenceLine(Schedule1, '9', 'Other income from Schedule 1', 0), - '7b': new ComputedLine((tr): number => { - return sumFormLines(tr, this, ['1', '2b', '3b', '4b', '4d', '5b', '6', '7a']); - }, 'Total income'), + '7b': new ComputedLine((tr) => this.totalIncome(tr), 'Total income'), '8a': new ReferenceLine(Schedule1, '22', 'Adjustments to income', 0), - '8b': new ComputedLine((tr): number => { - return this.getValue(tr, '7b') - this.getValue(tr, '8a'); - }, 'Adjusted gross income'), - - '9': new ComputedLine((tr): number => { - let deduction = 0; - const schedA = tr.findForm(ScheduleA); - if (schedA) { - deduction = schedA.getValue(tr, '17'); - if (schedA.getValue(tr, '18')) { - return deduction; - } - } + '8b': new ComputedLine((tr) => this.adjustedGrossIncome(tr), 'Adjusted gross income'), - return Math.max(deduction, tr.constants.standardDeduction[this.filingStatus]); - }, 'Deduction'), + '9': new ComputedLine((tr) => this.deduction(tr), 'Deduction'), - '10': new ComputedLine((tr): number => { - const f8995 = tr.findForm(Form8995REIT); - if (f8995) - return f8995.getValue(tr, '39'); - return 0; - }, 'Qualified business income deduction'), + '10': new ComputedLine((tr) => this.qualifiedBusinessIncomeDeduction(tr), 'Qualified business income deduction'), '11a': new ComputedLine((tr): number => { return this.getValue(tr, '9') + this.getValue(tr, '10'); }), - '11b': new ComputedLine((tr): number => { - return clampToZero(this.getValue(tr, '8b') - this.getValue(tr, '11a')); - }, 'Taxable income'), - - '12a': new ComputedLine((tr): number => { - // Not supported: - // Form 8814 (election to report child's interest or dividends) - // Form 4972 (relating to lump-sum distributions) - - const schedD = tr.findForm(ScheduleD); - // Line 18 and 19 are not undefined or 0. - if (schedD && !schedD.getValue(tr, '20')) { - // Use ScheD tax worksheet; - const schedDtw = tr.findForm(ScheduleDTaxWorksheet); - if (schedDtw) - return schedDtw.getValue(tr, '47'); - } - - // If there are qualified dividends, use the QDCGTW. - if (this.getValue(tr, '3a') > 0) { - const qdcgtw = tr.getForm(QDCGTaxWorksheet); - return qdcgtw.getValue(tr, '27'); - } + '11b': new ComputedLine((tr) => this.taxableIncome(tr), 'Taxable income'), - // Otherwise, compute just on taxable income. - return computeTax(this.getValue(tr, '11b'), tr); - }, 'Tax'), + '12a': new ComputedLine((tr) => this.tax(tr), 'Tax'), '12b': new ComputedLine((tr): number => { return this.getValue(tr, '12a') + tr.getForm(Schedule2).getValue(tr, '3'); @@ -219,13 +239,13 @@ export class QDCGTaxWorksheet extends Form { readonly name = 'QDCG Tax Worksheet'; readonly lines = { - '1': new ReferenceLine(Form1040, '11b', 'Taxable income'), + '1': new SymbolicLine(Form1040, 'taxableIncome', 'Taxable income'), '2': new ReferenceLine(Form1040, '3a', 'Qualified dividends'), '3': new ComputedLine((tr): number => { const schedD = tr.findForm(ScheduleD); if (schedD) return clampToZero(Math.min(schedD.getValue(tr, '15'), schedD.getValue(tr, '16'))); - return tr.getForm(Form1040).getValue(tr, '6'); + return tr.getForm(Form1040).capitalGainOrLoss(tr); }), '4': new ComputedLine((tr): number => this.getValue(tr, '2') + this.getValue(tr, '3')), '5': new UnsupportedLine('Form 4952@4g (Investment interest expense deduction)'), diff --git a/src/fed2019/Form1116.ts b/src/fed2019/Form1116.ts index d5dcdfd..1514b51 100644 --- a/src/fed2019/Form1116.ts +++ b/src/fed2019/Form1116.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-only import { Form, Person, TaxReturn } from '../core'; -import { ComputedLine, InputLine, ReferenceLine, UnsupportedLine, sumFormLines } from '../core/Line'; +import { Line, ComputedLine, InputLine, ReferenceLine, SymbolicLine, UnsupportedLine, sumFormLines } from '../core/Line'; import { UnsupportedFeatureError } from '../core/Errors'; import { reduceBySum } from '../core/Math'; @@ -48,7 +48,7 @@ export default class Form1116 extends Form { '1a': new Input('grossForeignIncome'), // 1b not supported - services as an employee. '2': new UnsupportedLine('Expenses definitely related to the income'), - '3a': new ReferenceLine(Form1040, '9', 'Deductions'), + '3a': new SymbolicLine(Form1040, 'deduction', 'Deductions'), '3b': new UnsupportedLine('Other deductions'), '3c': new ComputedLine((tr): number => { return this.getValue(tr, '3a') + this.getValue(tr, '3b'); @@ -58,7 +58,7 @@ export default class Form1116 extends Form { const f1040 = tr.findForm(Form1040); // Take total income, but do not net capital gains out with losses, so remove // line 6. - let grossIncome = f1040.getValue(tr, '7b') - f1040.getValue(tr, '6'); + let grossIncome = f1040.totalIncome(tr) - f1040.capitalGainOrLoss(tr); const f8949 = tr.findForm(Form8949); if (f8949) { const keys: (keyof Form8949['lines'])[] = ['boxA', 'boxB', 'boxC', 'boxD', 'boxE', 'boxF']; @@ -98,10 +98,10 @@ export default class Form1116 extends Form { '16': new UnsupportedLine('Adjustments to line 15'), '17': new ComputedLine((tr): number => this.getValue(tr, '15') + this.getValue(tr, '16')), // TODO - This does not handle necessary adjustments. - '18': new ReferenceLine(Form1040, '11b'), + '18': new SymbolicLine(Form1040, 'taxableIncome'), '19': new ComputedLine((tr): number => this.getValue(tr, '17') / this.getValue(tr, '18')), '20': new ComputedLine((tr): number => { - let value = tr.getForm(Form1040).getValue(tr, '12a'); + let value = tr.getForm(Form1040).tax(tr); const sched2 = tr.findForm(Schedule2); if (sched2) value += sched2.getValue(tr, '2'); diff --git a/src/fed2019/Form6251.ts b/src/fed2019/Form6251.ts index 11aa7d3..25e2621 100644 --- a/src/fed2019/Form6251.ts +++ b/src/fed2019/Form6251.ts @@ -21,14 +21,14 @@ export default class Form6251 extends Form { // Part I '1': new ComputedLine((tr): number => { const f1040 = tr.getForm(Form1040); - const l11b = f1040.getValue(tr, '11b'); - if (l11b > 0) - return l11b; - return f1040.getValue(tr, '8b') - f1040.getValue(tr, '9') - f1040.getValue(tr, '10'); + const income = f1040.taxableIncome(tr); + if (income > 0) + return income; + return f1040.adjustedGrossIncome(tr) - f1040.deduction(tr) - f1040.qualifiedBusinessIncomeDeduction(tr); }), '2a': new ComputedLine((tr): number => { // Not supported: Schedule A, line 7. - return tr.getForm(Form1040).getValue(tr, '9'); + return tr.getForm(Form1040).deduction(tr); }), '2b': new ReferenceLine(Schedule1, '1', 'Tax refund', 0), // Not supported - line 8 SALT. '2c': new UnsupportedLine('Investment interest expense'), @@ -82,7 +82,7 @@ export default class Form6251 extends Form { const f1040 = tr.getForm(Form1040); - let part3 = f1040.getValue(tr, '3a') > 0; + let part3 = f1040.qualifiedDividends(tr) > 0; const schedD = tr.findForm(ScheduleD); if (schedD) { @@ -100,7 +100,7 @@ export default class Form6251 extends Form { return this.getValue(tr, '7') - this.getValue(tr, '8'); }, 'Tentative minimum tax'), '10': new ComputedLine((tr): number => { - let value = tr.getForm(Form1040).getValue(tr, '12a'); + let value = tr.getForm(Form1040).tax(tr); const sched2 = tr.findForm(Schedule2); if (sched2) { value += sched2.getValue(tr, '2'); diff --git a/src/fed2019/Form8960.ts b/src/fed2019/Form8960.ts index c6be320..1e41aca 100644 --- a/src/fed2019/Form8960.ts +++ b/src/fed2019/Form8960.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-only import { Form, TaxReturn } from '../core'; -import { ComputedLine, ReferenceLine, UnsupportedLine, sumFormLines } from '../core/Line'; +import { Line, ComputedLine, ReferenceLine, SymbolicLine, UnsupportedLine, sumFormLines } from '../core/Line'; import { clampToZero, undefinedToZero } from '../core/Math'; import { Constants } from './TaxReturn'; @@ -17,14 +17,14 @@ export default class Form8960 extends Form { readonly lines = { // Part 1 // Section 6013 elections not supported. - '1': new ReferenceLine(Form1040, '2b', 'Taxable interest'), + '1': new SymbolicLine(Form1040, 'taxableInterest', 'Taxable interest'), '2': new ReferenceLine(Form1040, '3b', 'Ordinary dividends'), '3': new UnsupportedLine('Annuities'), '4a': new UnsupportedLine('Rental real estate, royalties, partnerships, S corporations, trusts, etc'), '4b': new UnsupportedLine('Adjustment for net income or loss derived in the ordinary course of a nonsection 1411 trade or business'), '4c': new ComputedLine((tr): number => this.getValue(tr, '4a') + this.getValue(tr, '4b')), '5a': new ComputedLine((tr): number => { - return (new ReferenceLine(Form1040, '6')).value(tr) + + return (new SymbolicLine(Form1040, 'capitalGainOrLoss')).value(tr) + undefinedToZero(new ReferenceLine(Schedule1, '4', undefined, 0).value(tr)); }, 'Net gain or loss'), '5b': new UnsupportedLine('Net gain or loss from disposition of property that is not subject to net investment income tax'), @@ -48,7 +48,7 @@ export default class Form8960 extends Form { // Part 3 '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'), + '13': new SymbolicLine(Form1040, 'adjustedGrossIncome', 'Modified adjusted gross income'), '14': new ComputedLine((tr): number => { return Form8960.filingStatusLimit(tr); }, 'Threshold'), diff --git a/src/fed2019/Form8995.ts b/src/fed2019/Form8995.ts index fbbc2d2..9dedfc2 100644 --- a/src/fed2019/Form8995.ts +++ b/src/fed2019/Form8995.ts @@ -32,12 +32,12 @@ export default class Form8995REIT extends Form { '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); - return f1040.getValue(tr, '8b') - f1040.getValue(tr, '9'); + return f1040.adjustedGrossIncome(tr) - f1040.deduction(tr); }, 'Taxable income before deduction'), '34': new ComputedLine((tr): number => { const f1040 = tr.getForm(Form1040); - let value = f1040.getValue(tr, '3a'); + let value = f1040.qualifiedDividends(tr); const schedD = tr.findForm(ScheduleD); if (schedD) { @@ -45,7 +45,7 @@ export default class Form8995REIT extends Form { clampToZero(schedD.getValue(tr, '15')), clampToZero(schedD.getValue(tr, '16'))); } else { - value += f1040.getValue(tr, '6'); + value += f1040.capitalGainOrLoss(tr); } return value; diff --git a/src/fed2019/Schedule1.ts b/src/fed2019/Schedule1.ts index deaa283..4f609ba 100644 --- a/src/fed2019/Schedule1.ts +++ b/src/fed2019/Schedule1.ts @@ -9,7 +9,7 @@ import * as Trace from '../core/Trace'; import { NotFoundError, UnsupportedFeatureError } from '../core/Errors'; import { undefinedToZero } from '../core/Math'; -import Form1040, { FilingStatus } from './Form1040'; +import { FilingStatus } from './Form1040'; export interface Schedule1Input { // Additional Income diff --git a/src/fed2019/Schedule3.ts b/src/fed2019/Schedule3.ts index c8e01cf..ca4ed79 100644 --- a/src/fed2019/Schedule3.ts +++ b/src/fed2019/Schedule3.ts @@ -31,7 +31,7 @@ export default class Schedule3 extends Form { if (totalForeignTax < limit) { const sched2l2 = new ReferenceLine(Schedule2, '2', undefined, 0); - return Math.min(totalForeignTax, f1040.getValue(tr, '12a') + sched2l2.value(tr)); + return Math.min(totalForeignTax, f1040.tax(tr) + sched2l2.value(tr)); } return tr.getForm(Form1116).getValue(tr, '33'); }, 'Foreign tax credit'), diff --git a/src/fed2019/ScheduleA.ts b/src/fed2019/ScheduleA.ts index af8946f..5b238ad 100644 --- a/src/fed2019/ScheduleA.ts +++ b/src/fed2019/ScheduleA.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-only import { Form } from '../core'; -import { ComputedLine, InputLine, ReferenceLine, UnsupportedLine, sumFormLines } from '../core/Line'; +import { Line, ComputedLine, InputLine, ReferenceLine, SymbolicLine, UnsupportedLine, sumFormLines } from '../core/Line'; import { clampToZero } from '../core/Math'; import Form1040, { FilingStatus } from './Form1040'; @@ -40,7 +40,7 @@ export default class ScheduleA extends Form { readonly lines = { // Medical and dental expenses '1': new Input('medicalAndDentalExpenses', 'Medical and dental expenses', 0), - '2': new ReferenceLine(Form1040, '8b'), + '2': new SymbolicLine(Form1040, 'adjustedGrossIncome'), '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'))), diff --git a/src/fed2019/ScheduleD.ts b/src/fed2019/ScheduleD.ts index 425608b..40824db 100644 --- a/src/fed2019/ScheduleD.ts +++ b/src/fed2019/ScheduleD.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-only import { Form, Person, TaxReturn } from '../core'; -import { Line, AccumulatorLine, ComputedLine, ReferenceLine, UnsupportedLine, sumFormLines, sumLineOfForms } from '../core/Line'; +import { Line, AccumulatorLine, ComputedLine, ReferenceLine, SymbolicLine, UnsupportedLine, sumFormLines, sumLineOfForms } from '../core/Line'; import { Literal, clampToZero } from '../core/Math'; import { NotFoundError, UnsupportedFeatureError } from '../core/Errors'; @@ -80,7 +80,7 @@ export default class ScheduleD extends Form { }, 'Net capital loss'), '22': new ComputedLine((tr): boolean => { - return tr.getForm(Form1040).getValue(tr, '3a') > 0; + return tr.getForm(Form1040).qualifiedDividends(tr) > 0; }, 'Need QD/CG Tax Worksheet'), }; }; @@ -89,8 +89,8 @@ export class ScheduleDTaxWorksheet extends Form { readonly name = 'Schedule D Tax Worksheet'; readonly lines = { - '1': new ReferenceLine(Form1040, '11b', 'Taxable income'), - '2': new ReferenceLine(Form1040, '3a', 'Qualified dividends'), + '1': new SymbolicLine(Form1040, 'taxableIncome', 'Taxable income'), + '2': new SymbolicLine(Form1040, 'qualifiedDividends', 'Qualified dividends'), '3': new UnsupportedLine('Form 4952@4g'), '4': new UnsupportedLine('Form 4952@4e'), '5': new ComputedLine((tr): number => 0), diff --git a/src/fed2020/Form1040.test.ts b/src/fed2020/Form1040.test.ts index 76f37b1..dc12c6f 100644 --- a/src/fed2020/Form1040.test.ts +++ b/src/fed2020/Form1040.test.ts @@ -19,6 +19,7 @@ test('standard deduction', () => { for (const filingStatus of Object.values(FilingStatus)) { const tr = new TaxReturn(); const f = new Form1040({ filingStatus }); + tr.addForm(f); expect(f.getValue(tr, '9')).toBe(filingStatusToResult[filingStatus]); } }); -- 2.22.5