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