Fix bugs in Schedule D and implement the QDCG Tax Worksheet.
[ustaxlib.git] / src / fed2019 / Form1040.ts
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
5
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';
10
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';
21
22 export enum FilingStatus {
23 Single = 'S',
24 MarriedFilingSeparate = 'MFS',
25 MarriedFilingJoint = 'MFJ',
26 };
27
28 export interface Form1040Input {
29 filingStatus: FilingStatus;
30 };
31
32 export default class Form1040 extends Form<Form1040['_lines'], Form1040Input> {
33 readonly name = '1040';
34
35 protected readonly _lines = {
36 '1': new AccumulatorLine(W2, '1', 'Wages, salaries, tips, etc.'),
37 '2a': new ComputedLine((tr): number => {
38 const value = (new AccumulatorLine(Form1099INT, '8')).value(tr) +
39 (new AccumulatorLine(Form1099DIV, '11')).value(tr);
40 return value;
41 }, 'Tax-exempt interest'),
42 '2b': new AccumulatorLine(Form1099INT, '1', 'Taxable interest'),
43 '3a': new AccumulatorLine(Form1099DIV, '1b', 'Qualified dividends'),
44 '3b': new AccumulatorLine(Form1099DIV, '1a', 'Ordinary dividends'),
45 '4a': new ComputedLine((tr): number => {
46 const f1099Rs = tr.findForms(Form1099R).filter(f => !f.getValue(tr, '7').includes(Box7Code.G));
47 return sumLineOfForms(tr, f1099Rs, '1');
48 }),
49 '4b': new ComputedLine((tr): number => {
50 const f8606s = tr.findForms(Form8606);
51 return sumLineOfForms(tr, f8606s, '15c') + sumLineOfForms(tr, f8606s, '18');
52 }, 'IRA distributions, taxadble amount'),
53 '4d': new ComputedLine(() => 0),
54 // 4c and 4d are not supported
55 // 5a and 5b are not supported
56 '6': new ComputedLine((tr): number => {
57 const schedD = tr.findForm(ScheduleD);
58 if (!schedD)
59 return 0;
60
61 const l6 = schedD.getValue(tr, '16');
62 if (l6 >= 0)
63 return l6;
64 return schedD.getValue(tr, '21');
65 }, 'Capital gain/loss'),
66 '7a': new ReferenceLine(Schedule1, '9', 'Other income from Schedule 1', 0),
67
68 '7b': new ComputedLine((tr): number => {
69 let income = 0;
70 income += this.getValue(tr, '1');
71 income += this.getValue(tr, '2b');
72 income += this.getValue(tr, '3b');
73 income += this.getValue(tr, '4b');
74 income += this.getValue(tr, '4d');
75 //income += this.getValue(tr, '5b');
76 income += this.getValue(tr, '6');
77 income += this.getValue(tr, '7a');
78 return income;
79 }, 'Total income'),
80
81 '8a': new ReferenceLine(Schedule1, '22', 'Adjustments to income', 0),
82
83 '8b': new ComputedLine((tr): number => {
84 return this.getValue(tr, '7b') - this.getValue(tr, '8a');
85 }, 'Adjusted gross income'),
86
87 '9': new ComputedLine((): number => {
88 // TODO - Itemized deductions.
89 switch (this.filingStatus) {
90 case FilingStatus.Single:
91 case FilingStatus.MarriedFilingSeparate:
92 return 12200;
93 case FilingStatus.MarriedFilingJoint:
94 return 24400;
95 }
96 }, 'Deduction'),
97
98 '10': new ComputedLine((tr): number => {
99 const taxableIncome = this.getValue(tr, '8b');
100 let use8995a = false;
101 switch (this.filingStatus) {
102 case FilingStatus.Single: use8995a = taxableIncome <= 160700; break;
103 case FilingStatus.MarriedFilingSeparate: use8995a = taxableIncome <= 160725; break;
104 case FilingStatus.MarriedFilingJoint: use8995a = taxableIncome <= 321400; break;
105 };
106 return 0;
107 }, 'Qualified business income deduction'),
108
109 '11a': new ComputedLine((tr): number => {
110 return this.getValue(tr, '9') + this.getValue(tr, '10');
111 }),
112 '11b': new ComputedLine((tr): number => {
113 return clampToZero(this.getValue(tr, '8b') - this.getValue(tr, '11a'));
114 }, 'Taxable income'),
115
116 '12a': new ComputedLine((tr): number => {
117 // Not supported:
118 // Form 8814 (election to report child's interest or dividends)
119 // Form 4972 (relating to lump-sum distributions)
120
121 const schedD = tr.findForm(ScheduleD);
122 // Line 18 and 19 are not undefined or 0.
123 if (schedD && !schedD.getValue(tr, '20')) {
124 // Use ScheD tax worksheet;
125 const schedDtw = tr.findForm(ScheduleDTaxWorksheet);
126 if (schedDtw)
127 return schedDtw.getValue(tr, '47');
128 }
129
130 // If there are qualified dividends, use the QDCGTW.
131 if (this.getValue(tr, '3a') > 0) {
132 const qdcgtw = tr.getForm(QDCGTaxWorksheet);
133 return qdcgtw.getValue(tr, '27');
134 }
135
136 // Otherwise, compute just on taxable income.
137 return computeTax(this.getValue(tr, '11b'), this.filingStatus);
138 }, 'Tax'),
139
140 '12b': new ComputedLine((tr): number => {
141 return this.getValue(tr, '12a') + tr.getForm(Schedule2).getValue(tr, '3');
142 }, 'Additional tax'),
143
144 // Not supported: 13a - child tax credit
145
146 '13b': new ReferenceLine(Schedule3, '7', 'Additional credits', 0),
147
148 '14': new ComputedLine((tr): number => {
149 return clampToZero(this.getValue(tr, '12b') - this.getValue(tr, '13b'));
150 }),
151
152 '15': new ReferenceLine(Schedule2, '10', undefined, 0),
153
154 '16': new ComputedLine((tr): number => {
155 return this.getValue(tr, '14') + this.getValue(tr, '15');
156 }, 'Total tax'),
157
158 '17': new ComputedLine((tr): number => {
159 const fedTaxWithheldBoxes = [
160 new AccumulatorLine(W2, '2'),
161 new AccumulatorLine(Form1099R, '4'),
162 new AccumulatorLine(Form1099DIV, '4'),
163 new AccumulatorLine(Form1099INT, '4'),
164 ];
165 const withholding: number[] = fedTaxWithheldBoxes.map(b => b.value(tr));
166
167 let additionalMedicare = 0;
168 const f8959 = tr.findForm(Form8959)
169 if (f8959) {
170 additionalMedicare = f8959.getValue(tr, '24');
171 }
172
173 return reduceBySum(withholding) + additionalMedicare;
174 }, 'Federal income tax withheld'),
175
176 // 18a not supported - Earned income credit (EIC)
177 // 18b not supported - Additional child tax credit. Attach Schedule 8812
178 // 18c not supported - American opportunity credit from Form 8863, line 8
179 '18d': new ReferenceLine(Schedule3, '14', undefined, 0),
180 '18e': new ComputedLine((tr): number => {
181 // Should include 18a-18c.
182 return this.getValue(tr, '18d');
183 }),
184
185 '19': new ComputedLine((tr): number => {
186 return this.getValue(tr, '17') + this.getValue(tr, '18e');
187 }, 'Total payments'),
188
189 '20': new ComputedLine((tr): number => {
190 return clampToZero(this.getValue(tr, '19') - this.getValue(tr, '16'));
191 }, 'Amount overpaid'),
192
193 '23': new ComputedLine((tr): number => {
194 return clampToZero(this.getValue(tr, '16') - this.getValue(tr, '19'));
195 }, 'Amount you owe'),
196 }
197
198 get filingStatus(): FilingStatus {
199 return this.getInput('filingStatus');
200 }
201 };
202
203 export function computeTax(income: number, filingStatus: FilingStatus): number {
204 // From https://www.irs.gov/pub/irs-drop/rp-18-57.pdf, Section 3.01 and
205 // https://www.irs.gov/pub/irs-pdf/p17.pdf, 2019 Tax Rate Schedules (p254).
206 const taxBrackets = {
207 // Format is:
208 // [ limit-of-taxable-income, marginal-rate, base-tax ]
209 // If Income is over Row[0], pay Row[2] + (Row[1] * (Income - PreviousRow[0]))
210 [FilingStatus.MarriedFilingJoint]: [
211 [ 19400, 0.10, 0 ],
212 [ 78950, 0.12, 1940 ],
213 [ 168400, 0.22, 9086 ],
214 [ 321450, 0.24, 28765 ],
215 [ 408200, 0.32, 65497 ],
216 [ 612350, 0.35, 93257 ],
217 [ Infinity, 0.37, 164709.50 ]
218 ],
219 [FilingStatus.Single]: [
220 [ 9700, 0.10, 0 ],
221 [ 39475, 0.12, 970 ],
222 [ 84200, 0.22, 4543 ],
223 [ 160725, 0.24, 14382.50 ],
224 [ 204100, 0.32, 32748.50 ],
225 [ 510300, 0.35, 46628.50 ],
226 [ Infinity, 0.37, 153798.50 ]
227 ],
228 [FilingStatus.MarriedFilingSeparate]: [
229 [ 9700, 0.10, 0 ],
230 [ 39475, 0.12, 970 ],
231 [ 84200, 0.22, 4543 ],
232 [ 160725, 0.24, 14382.50 ],
233 [ 204100, 0.32, 32748.50 ],
234 [ 306175, 0.35, 46628.50 ],
235 [ Infinity, 0.37, 82354.75 ]
236 ]
237 }[filingStatus];
238
239 let i = 0;
240 while (taxBrackets[i][0] < income)
241 ++i;
242
243 const bracket = taxBrackets[i];
244 const bracketStart = i == 0 ? 0 : taxBrackets[i - 1][0];
245
246 return ((income - bracketStart) * bracket[1]) + bracket[2];
247 };
248
249 export class QDCGTaxWorksheet extends Form<QDCGTaxWorksheet['_lines']> {
250 readonly name = 'Qual Div Cap Gain Tax Worksheet';
251
252 protected readonly _lines = {
253 '1': new ReferenceLine(Form1040, '11b', 'Taxable income'),
254 '2': new ReferenceLine(Form1040, '3a', 'Qualified dividends'),
255 '3': new ComputedLine((tr): number => {
256 const schedD = tr.findForm(ScheduleD);
257 if (schedD)
258 return Math.min(schedD.getValue(tr, '15'), schedD.getValue(tr, '16'));
259 return tr.getForm(Form1040).getValue(tr, '6');
260 }),
261 '4': new ComputedLine((tr): number => this.getValue(tr, '2') + this.getValue(tr, '3')),
262 '5': new ComputedLine(() => 0), // Not supported - Form 4952/4g (nvestment interest expense deduction)
263 '6': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '4') - this.getValue(tr, '5'))),
264 '7': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '6'))),
265 '8': new ComputedLine((tr): number => {
266 switch (tr.getForm(Form1040).filingStatus) {
267 case FilingStatus.Single:
268 case FilingStatus.MarriedFilingSeparate:
269 return 39375;
270 case FilingStatus.MarriedFilingJoint:
271 return 78750;
272 };
273 }),
274 '9': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '8'))),
275 '10': new ComputedLine((tr): number => Math.min(this.getValue(tr, '7'), this.getValue(tr, '9'))),
276 '11': new ComputedLine((tr): number => {
277 return this.getValue(tr, '9') - this.getValue(tr, '10');
278 }, 'Amount taxed at 0%'),
279 '12': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '6'))),
280 '13': new ReferenceLine(QDCGTaxWorksheet as any, '11'),
281 '14': new ComputedLine((tr): number => this.getValue(tr, '12') - this.getValue(tr, '13')),
282 '15': new ComputedLine((tr): number => {
283 switch (tr.getForm(Form1040).filingStatus) {
284 case FilingStatus.Single: return 434550;
285 case FilingStatus.MarriedFilingSeparate: return 244425;
286 case FilingStatus.MarriedFilingJoint: return 488850;
287 };
288 }),
289 '16': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '15'))),
290 '17': new ComputedLine((tr): number => this.getValue(tr, '7') + this.getValue(tr, '11')),
291 '18': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '16') - this.getValue(tr, '17'))),
292 '19': new ComputedLine((tr): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '18'))),
293 '20': new ComputedLine((tr): number => {
294 return this.getValue(tr, '19') * 0.15;
295 }, 'Amount taxed at 15%'),
296 '21': new ComputedLine((tr): number => this.getValue(tr, '11') + this.getValue(tr, '19')),
297 '22': new ComputedLine((tr): number => this.getValue(tr, '12') - this.getValue(tr, '21')),
298 '23': new ComputedLine((tr): number => {
299 return this.getValue(tr, '22') * 0.20;
300 }, 'Amount taxed at 20%'),
301 '24': new ComputedLine((tr): number => {
302 return computeTax(this.getValue(tr, '7'), tr.getForm(Form1040).filingStatus);
303 }, 'Tax on line 7'),
304 '25': new ComputedLine((tr): number => {
305 return this.getValue(tr, '20') +
306 this.getValue(tr, '23') +
307 this.getValue(tr, '24');
308 }),
309 '26': new ComputedLine((tr): number => {
310 return computeTax(this.getValue(tr, '1'), tr.getForm(Form1040).filingStatus);
311 }, 'Tax on line 1'),
312 '27': new ComputedLine((tr): number => {
313 return Math.min(this.getValue(tr, '25'), this.getValue(tr, '26'));
314 }, 'Tax on all taxable income')
315 };
316 };