}
};
+// SymbolicLine cannot be used for lines defined on F itself. For those cases, use:
+// new ComputedLine((tr) => this.K(tr));
+export class SymbolicLine<F extends Form & { [key in K]: ComputeFunc<ReturnType<F[K]>> },
+ K extends keyof F>
+ extends Line<ReturnType<F[K]>> {
+ private _form: FormClass<F>;
+ private _key: K;
+
+ constructor(form: FormClass<F>, key: K, description?: string) {
+ super(description || `Reference ${form.name}/${key}`);
+ this._form = form;
+ this._key = key;
+ }
+
+ value(tr: TaxReturn): ReturnType<F[K]> {
+ Trace.begin(this);
+ const form: F = tr.findForm(this._form);
+ const value = form[this._key](tr);
+ Trace.end();
+ return value;
+ }
+}
+
export class InputLine<U = unknown, T extends keyof U = any> extends Line<U[T]> {
private _input: T;
private _fallback: U[T];
// 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';
export default class Form1040 extends Form<Form1040Input> {
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 => {
(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 => {
'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');
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)'),
// 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';
'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');
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'];
'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');
// 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'),
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) {
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');
// 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';
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'),
// 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'),
'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) {
clampToZero(schedD.getValue(tr, '15')),
clampToZero(schedD.getValue(tr, '16')));
} else {
- value += f1040.getValue(tr, '6');
+ value += f1040.capitalGainOrLoss(tr);
}
return value;
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
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'),
// 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';
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'))),
// 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';
}, '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'),
};
};
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),
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]);
}
});