1 // Copyright 2020 Blue Static <https://www.bluestatic.org>
2 // This program is free software licensed under the GNU General Public License,
3 // version 3.0. The full text of the license can be found in LICENSE.txt.
4 // SPDX-License-Identifier: GPL-3.0-only
6 import { Form, TaxReturn } from '../core';
7 import { Line, AccumulatorLine, ComputedLine, ReferenceLine, sumLineOfForms } from '../core/Line';
8 import { UnsupportedFeatureError } from '../core/Errors';
9 import { clampToZero, reduceBySum } from '../core/Math';
11 import Form8606 from './Form8606';
12 import Form8959 from './Form8959';
13 import Form1099INT from './Form1099INT';
14 import Form1099DIV from './Form1099DIV';
15 import Form1099R, { Box7Code } from './Form1099R';
16 import W2 from './W2';
17 import Schedule1 from './Schedule1';
18 import Schedule2 from './Schedule2';
19 import Schedule3 from './Schedule3';
20 import ScheduleD, { ScheduleDTaxWorksheet } from './ScheduleD';
22 export enum FilingStatus {
24 MarriedFilingSeparate = 'MFS',
25 MarriedFilingJoint = 'MFJ',
28 export interface Form1040Input {
29 filingStatus: FilingStatus;
32 export default class Form1040 extends Form<Form1040['_lines'], Form1040Input> {
33 readonly name = '1040';
35 protected readonly _lines = {
36 '1': new AccumulatorLine(W2, '1', 'Wages, salaries, tips, etc.'),
37 '2a': new AccumulatorLine(Form1099INT, '8', 'Tax-exempt interest'),
38 '2b': new AccumulatorLine(Form1099INT, '1', 'Taxable interest'),
39 '3a': new AccumulatorLine(Form1099DIV, '1b', 'Qualified dividends'),
40 '3b': new AccumulatorLine(Form1099DIV, '1a', 'Ordinary dividends'),
41 '4a': new ComputedLine((tr): number => {
42 const f1099Rs = tr.findForms(Form1099R).filter(f => !f.getValue(tr, '7').includes(Box7Code.G));
43 return sumLineOfForms(tr, f1099Rs, '1');
45 '4b': new ComputedLine((tr): number => {
46 const f8606s = tr.findForms(Form8606);
47 return sumLineOfForms(tr, f8606s, '15c') + sumLineOfForms(tr, f8606s, '18');
48 }, 'IRA distributions, taxadble amount'),
49 '4d': new ComputedLine(() => 0),
50 // 4c and 4d are not supported
51 // 5a and 5b are not supported
52 '6': new ComputedLine((tr): number => {
53 const schedD = tr.findForm(ScheduleD);
57 const l6 = schedD.getValue(tr, '16');
60 return schedD.getValue(tr, '21');
61 }, 'Capital gain/loss'),
62 '7a': new ReferenceLine(Schedule1, '9', 'Other income from Schedule 1', 0),
64 '7b': new ComputedLine((tr): number => {
66 income += this.getValue(tr, '1');
67 income += this.getValue(tr, '2b');
68 income += this.getValue(tr, '3b');
69 income += this.getValue(tr, '4b');
70 income += this.getValue(tr, '4d');
71 //income += this.getValue(tr, '5b');
72 income += this.getValue(tr, '6');
73 income += this.getValue(tr, '7a');
77 '8a': new ReferenceLine(Schedule1, '22', 'Adjustments to income', 0),
79 '8b': new ComputedLine((tr): number => {
80 return this.getValue(tr, '7b') - this.getValue(tr, '8a');
81 }, 'Adjusted gross income'),
83 '9': new ComputedLine((): number => {
84 // TODO - Itemized deductions.
85 switch (this.filingStatus) {
86 case FilingStatus.Single:
87 case FilingStatus.MarriedFilingSeparate:
89 case FilingStatus.MarriedFilingJoint:
94 '10': new ComputedLine((tr): number => {
95 const taxableIncome = this.getValue(tr, '8b');
97 switch (this.filingStatus) {
98 case FilingStatus.Single: use8995a = taxableIncome <= 160700; break;
99 case FilingStatus.MarriedFilingSeparate: use8995a = taxableIncome <= 160725; break;
100 case FilingStatus.MarriedFilingJoint: use8995a = taxableIncome <= 321400; break;
103 }, 'Qualified business income deduction'),
105 '11a': new ComputedLine((tr): number => {
106 return this.getValue(tr, '9') + this.getValue(tr, '10');
108 '11b': new ComputedLine((tr): number => {
109 return clampToZero(this.getValue(tr, '8b') - this.getValue(tr, '11a'));
110 }, 'Taxable income'),
112 '12a': new ComputedLine((tr): number => {
114 // Form 8814 (election to report child's interest or dividends)
115 // Form 4972 (relating to lump-sum distributions)
116 const taxableIncome = this.getValue(tr, '11b');
118 if (this.getValue(tr, '3a') > 0 && !tr.findForm(ScheduleD))
119 throw new UnsupportedFeatureError('Qualified Dividends and Captial Gains Tax Worksheet not supported, Schedule D requried');
121 const schedD = tr.findForm(ScheduleDTaxWorksheet);
123 return schedD.getValue(tr, '47');
125 return computeTax(taxableIncome, this.filingStatus);
128 '12b': new ComputedLine((tr): number => {
129 return this.getValue(tr, '12a') + tr.getForm(Schedule2).getValue(tr, '3');
130 }, 'Additional tax'),
132 // Not supported: 13a - child tax credit
134 '13b': new ReferenceLine(Schedule3, '7', 'Additional credits', 0),
136 '14': new ComputedLine((tr): number => {
137 return clampToZero(this.getValue(tr, '12b') - this.getValue(tr, '13b'));
140 '15': new ReferenceLine(Schedule2, '10', undefined, 0),
142 '16': new ComputedLine((tr): number => {
143 return this.getValue(tr, '14') + this.getValue(tr, '15');
146 '17': new ComputedLine((tr): number => {
147 const fedTaxWithheldBoxes = [
148 new AccumulatorLine(W2, '2'),
149 new AccumulatorLine(Form1099R, '4'),
150 new AccumulatorLine(Form1099DIV, '4'),
151 new AccumulatorLine(Form1099INT, '4'),
153 const withholding: number[] = fedTaxWithheldBoxes.map(b => b.value(tr));
155 let additionalMedicare = 0;
156 const f8959 = tr.findForm(Form8959)
158 additionalMedicare = f8959.getValue(tr, '24');
161 return reduceBySum(withholding) + additionalMedicare;
162 }, 'Federal income tax withheld'),
164 // 18a not supported - Earned income credit (EIC)
165 // 18b not supported - Additional child tax credit. Attach Schedule 8812
166 // 18c not supported - American opportunity credit from Form 8863, line 8
167 '18d': new ReferenceLine(Schedule3, '14', undefined, 0),
168 '18e': new ComputedLine((tr): number => {
169 // Should include 18a-18c.
170 return this.getValue(tr, '18d');
173 '19': new ComputedLine((tr): number => {
174 return this.getValue(tr, '17') + this.getValue(tr, '18e');
175 }, 'Total payments'),
177 '20': new ComputedLine((tr): number => {
178 return clampToZero(this.getValue(tr, '19') - this.getValue(tr, '16'));
179 }, 'Amount overpaid'),
181 '23': new ComputedLine((tr): number => {
182 return clampToZero(this.getValue(tr, '16') - this.getValue(tr, '19'));
183 }, 'Amount you owe'),
186 get filingStatus(): FilingStatus {
187 return this.getInput('filingStatus');
191 export function computeTax(income: number, filingStatus: FilingStatus): number {
192 // From https://www.irs.gov/pub/irs-drop/rp-18-57.pdf, Section 3.01 and
193 // https://www.irs.gov/pub/irs-pdf/p17.pdf, 2019 Tax Rate Schedules (p254).
194 const taxBrackets = {
196 // [ limit-of-taxable-income, marginal-rate, base-tax ]
197 // If Income is over Row[0], pay Row[2] + (Row[1] * (Income - PreviousRow[0]))
198 [FilingStatus.MarriedFilingJoint]: [
200 [ 78950, 0.12, 1940 ],
201 [ 168400, 0.22, 9086 ],
202 [ 321450, 0.24, 28765 ],
203 [ 408200, 0.32, 65497 ],
204 [ 612350, 0.35, 93257 ],
205 [ Infinity, 0.37, 164709.50 ]
207 [FilingStatus.Single]: [
209 [ 39475, 0.12, 970 ],
210 [ 84200, 0.22, 4543 ],
211 [ 160725, 0.24, 14382.50 ],
212 [ 204100, 0.32, 32748.50 ],
213 [ 510300, 0.35, 46628.50 ],
214 [ Infinity, 0.37, 153798.50 ]
216 [FilingStatus.MarriedFilingSeparate]: [
218 [ 39475, 0.12, 970 ],
219 [ 84200, 0.22, 4543 ],
220 [ 160725, 0.24, 14382.50 ],
221 [ 204100, 0.32, 32748.50 ],
222 [ 306175, 0.35, 46628.50 ],
223 [ Infinity, 0.37, 82354.75 ]
228 while (taxBrackets[i][0] < income)
231 const bracket = taxBrackets[i];
232 const bracketStart = i == 0 ? 0 : taxBrackets[i - 1][0];
234 return ((income - bracketStart) * bracket[1]) + bracket[2];