Add Form1099R and 8606.
[ustaxlib.git] / src / fed2019 / Form1040.ts
1 import Form, { FormClass } from '../Form';
2 import TaxReturn from '../TaxReturn';
3 import { Line, AccumulatorLine, ComputedLine, ReferenceLine, sumLineOfForms } from '../Line';
4 import { UnsupportedFeatureError } from '../Errors';
5
6 import Form8606 from './Form8606';
7 import Form8959 from './Form8959';
8 import Form1099INT from './Form1099INT';
9 import Form1099DIV from './Form1099DIV';
10 import Form1099R, { Box7Code } from './Form1099R';
11 import FormW2 from './FormW2';
12 import ScheduleD, { ScheduleDTaxWorksheet } from './ScheduleD';
13
14 export enum FilingStatus {
15 Single = 'S',
16 MarriedFilingSeparate = 'MFS',
17 MarriedFilingJoint = 'MFJ',
18 };
19
20 export interface Form1040Input {
21 filingStatus: FilingStatus;
22 };
23
24 const reduceBySum = (list: number[]) => list.reduce((acc, curr) => acc + curr, 0);
25
26 export default class Form1040 extends Form<Form1040['_lines'], Form1040Input> {
27 readonly name = '1040';
28
29 protected readonly _lines = {
30 '1': new AccumulatorLine(FormW2, '1', 'Wages, salaries, tips, etc.'),
31 '2a': new AccumulatorLine(Form1099INT, '8', 'Tax-exempt interest'),
32 '2b': new AccumulatorLine(Form1099INT, '1', 'Taxable interest'),
33 '3a': new AccumulatorLine(Form1099DIV, '1b', 'Qualified dividends'),
34 '3b': new AccumulatorLine(Form1099DIV, '1a', 'Ordinary dividends'),
35 '4a': new ComputedLine((tr): number => {
36 const f1099Rs = tr.findForms(Form1099R).filter(f => !f.getValue(tr, '7').includes(Box7Code.G));
37 return sumLineOfForms(tr, f1099Rs, '1');
38 }),
39 '4b': new ComputedLine((tr): number => {
40 const f8606s = tr.findForms(Form8606);
41 return sumLineOfForms(tr, f8606s, '15c') + sumLineOfForms(tr, f8606s, '18');
42 }, 'IRA distributions, taxadble amount'),
43 '4d': new ComputedLine(() => 0),
44 // 4c and 4d are not supported
45 // 5a and 5b are not supported
46 '6': new ComputedLine((tr): number => {
47 const schedD = tr.findForm(ScheduleD);
48 if (!schedD)
49 return 0;
50
51 const l6 = schedD.getValue(tr, '16');
52 if (l6 > 0)
53 return l6;
54 return schedD.getValue(tr, '21');
55 }, 'Capital gain/loss'),
56 '7a': new ReferenceLine(/*'Schedule 1'*/ undefined, '9', 'Other income from Schedule 1', 0),
57
58 '7b': new ComputedLine((tr): number => {
59 let income = 0;
60 income += this.getValue(tr, '1');
61 income += this.getValue(tr, '2b');
62 income += this.getValue(tr, '3b');
63 income += this.getValue(tr, '4b');
64 income += this.getValue(tr, '4d');
65 //income += this.getValue(tr, '5b');
66 income += this.getValue(tr, '6');
67 income += this.getValue(tr, '7a');
68 return income;
69 }, 'Total income'),
70
71 '8a': new ReferenceLine(undefined /*'Schedule 1'*/, '22', 'Adjustments to income', 0),
72
73 '8b': new ComputedLine((tr): number => {
74 return this.getValue(tr, '7b') - this.getValue(tr, '8a');
75 }, 'Adjusted gross income'),
76
77 // TODO - Deduction
78 '9': new ComputedLine(() => 0, 'Deduction'),
79
80 '10': new ComputedLine((tr): number => {
81 const taxableIncome = this.getValue(tr, '8b');
82 let use8995a = false;
83 switch (this.getInput('filingStatus')) {
84 case FilingStatus.Single: use8995a = taxableIncome <= 160700; break;
85 case FilingStatus.MarriedFilingSeparate: use8995a = taxableIncome <= 160725; break;
86 case FilingStatus.MarriedFilingJoint: use8995a = taxableIncome <= 321400; break;
87 };
88 return 0;
89 }, 'Qualified business income deduction'),
90
91 '11a': new ComputedLine((tr): number => {
92 return this.getValue(tr, '9') + this.getValue(tr, '10');
93 }),
94 '11b': new ComputedLine((tr): number => {
95 const value = this.getValue(tr, '8b') - this.getValue(tr, '11a');
96 return value < 0 ? 0 : value;
97 }, 'Taxable income'),
98
99 '12a': new ComputedLine((tr): number => {
100 // Not supported:
101 // Form 8814 (election to report child's interest or dividends)
102 // Form 4972 (relating to lump-sum distributions)
103 const taxableIncome = this.getValue(tr, '11b');
104
105 if (this.getValue(tr, '3a') > 0 && !tr.findForm(ScheduleD))
106 throw new UnsupportedFeatureError('Qualified Dividends and Captial Gains Tax Worksheet not supported, Schedule D requried');
107
108 const schedD = tr.findForm(ScheduleDTaxWorksheet);
109 if (schedD)
110 return schedD.getValue(tr, '47');
111
112 return computeTax(taxableIncome, this.getInput('filingStatus'));
113 }, 'Tax'),
114
115 '12b': new ComputedLine((tr): number => {
116 return this.getValue(tr, '12a') + tr.getForm(Schedule2).getValue(tr, '3');
117 }, 'Additional tax'),
118
119 // Not supported: 13a - child tax credit
120
121 '13b': new ComputedLine((tr): number => {
122 // TODO: add Sched 3.L7
123 return 0;
124 }, 'Additional credits'),
125
126 '14': new ComputedLine((tr): number => {
127 const l12b = this.getValue(tr, '12b');
128 const l13b = this.getValue(tr, '13b');
129 const value = l12b - l13b;
130 return value < 0 ? 0 : value;
131 }),
132
133 '15': new ReferenceLine(Schedule2, '10', undefined, 0),
134
135 '16': new ComputedLine((tr): number => {
136 return this.getValue(tr, '14') + this.getValue(tr, '15');
137 }, 'Total tax'),
138
139 '17': new ComputedLine((tr): number => {
140 const fedTaxWithheldBoxes = [
141 new AccumulatorLine(FormW2, '2'),
142 new AccumulatorLine(Form1099R, '4'),
143 new AccumulatorLine(Form1099DIV, '4'),
144 new AccumulatorLine(Form1099INT, '4'),
145 ];
146 const withholding: number[] = fedTaxWithheldBoxes.map(b => b.value(tr));
147
148 let additionalMedicare = 0;
149 const f8959 = tr.findForm(Form8959)
150 if (f8959) {
151 additionalMedicare = f8959.getValue(tr, '24');
152 }
153
154 return reduceBySum(withholding) + additionalMedicare;
155 }, 'Federal income tax withheld'),
156
157 // 18 not supported
158
159 '19': new ReferenceLine(Form1040 as any, '17', 'Total payments'),
160
161 '20': new ComputedLine((tr): number => {
162 const l16: number = this.getValue(tr, '16');
163 const l19: number = this.getValue(tr, '19');
164 if (l19 > l16)
165 return l19 - l16;
166 return 0;
167 }, 'Amount overpaid'),
168
169 '23': new ComputedLine((tr): number => {
170 const l16 = this.getValue(tr, '16');
171 const l19 = this.getValue(tr, '19');
172 if (l19 < l16)
173 return l16 - l19;
174 return 0;
175 }, 'Amount you owe'),
176 };
177 };
178
179 export class Schedule2 extends Form<Schedule2['_lines']> {
180 readonly name = 'Schedule 2';
181
182 protected readonly _lines = {
183 '1': new ComputedLine((tr): number => {
184 // TODO - this is just using Taxable Income, rather than AMT-limited
185 // income
186 const f1040 = tr.getForm(Form1040);
187 const taxableIncome = f1040.getValue(tr, '11b');
188 switch (f1040.getInput('filingStatus')) {
189 case FilingStatus.Single:
190 if (taxableIncome < 510300)
191 return 0;
192 case FilingStatus.MarriedFilingJoint:
193 if (taxableIncome < 1020600)
194 return 0;
195 case FilingStatus.MarriedFilingSeparate:
196 if (taxableIncome < 510300)
197 return 0;
198 }
199 throw new UnsupportedFeatureError('The AMT is not supported');
200 }, 'AMT'),
201 // 2 is not supported (Excess advance premium tax credit repayment)
202 '3': new ComputedLine((tr): number => {
203 // Should include line 2.
204 return this.getValue(tr, '1');
205 }),
206
207 // 4 is not supported (Self-employment tax.)
208 // 5 is not supported (Unreported social security and Medicare tax from)
209 // 6 is not supported (Additional tax on IRAs, other qualified retirement plans, and other tax-favored accounts)
210 // 7 is not supported (Household employment taxes.)
211 '8': new ComputedLine((tr): number => {
212 const f1040 = tr.getForm(Form1040);
213 const wages = f1040.getLine('1').value(tr);
214
215 let niit: boolean;
216 const filingStatus = f1040.getInput('filingStatus');
217
218 const additionalMedicare = wages > Form8959.filingStatusLimit(filingStatus);
219
220 switch (f1040.getInput('filingStatus')) {
221 case FilingStatus.Single:
222 if (wages > 200000) {
223 niit = true;
224 }
225 break;
226 case FilingStatus.MarriedFilingJoint:
227 if (wages > 250000) {
228 niit = true;
229 }
230 break;
231 case FilingStatus.MarriedFilingSeparate:
232 if (wages > 125000) {
233 niit = true;
234 }
235 break;
236 }
237
238 let value = 0;
239
240 if (additionalMedicare) {
241 const f8959 = tr.getForm(Form8959);
242 value += f8959.getValue(tr, '18');
243 }
244
245 if (niit) {
246 //const f8960 = tr.getForm('8960');
247 }
248
249 return value;
250 }),
251 // 9 is not supported (Section 965 net tax liability installment from Form 965-A)
252
253 '10': new ComputedLine((tr): number => {
254 // Should be lines 4 - 8.
255 return this.getValue(tr, '8');
256 })
257 };
258 };
259
260 export function computeTax(income: number, filingStatus: FilingStatus): number {
261 if (income < 100000)
262 throw new UnsupportedFeatureError('Tax-table tax liability not supported');
263
264 switch (filingStatus) {
265 case FilingStatus.Single:
266 if (income < 160725)
267 return (income * 0.24) - 5825.50;
268 else if (income < 204100)
269 return (income * 0.32) - 18683.50;
270 else if (income < 510300)
271 return (income * 0.35) - 24806.50;
272 else
273 return (income * 0.38) - 35012.50;
274 case FilingStatus.MarriedFilingJoint:
275 if (income < 168400)
276 return (income * 0.22) - 8283.00;
277 else if (income < 321450)
278 return (income * 0.24) - 11651.00;
279 else if (income < 408200)
280 return (income * 0.32) - 37367.00;
281 else if (income < 612350)
282 return (income * 0.35) - 49613.00;
283 else
284 return (income * 0.37) - 61860.00;
285 case FilingStatus.MarriedFilingSeparate:
286 if (income < 160725)
287 return (income * 0.24) - 5825.50;
288 else if (income < 204100)
289 return (income * 0.32) - 18683.50;
290 else if (income < 306175)
291 return (income * 0.35) - 24806.50;
292 else
293 return (income * 0.37) - 30930.00;
294 }
295 throw new UnsupportedFeatureError('Unexpected return type');
296 };