Split Schedule2 into its own file.
[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 import { reduceBySum } from '../Math';
6
7 import Form8606 from './Form8606';
8 import Form8959 from './Form8959';
9 import Form1099INT from './Form1099INT';
10 import Form1099DIV from './Form1099DIV';
11 import Form1099R, { Box7Code } from './Form1099R';
12 import FormW2 from './FormW2';
13 import Schedule2 from './Schedule2';
14 import ScheduleD, { ScheduleDTaxWorksheet } from './ScheduleD';
15
16 export enum FilingStatus {
17 Single = 'S',
18 MarriedFilingSeparate = 'MFS',
19 MarriedFilingJoint = 'MFJ',
20 };
21
22 export interface Form1040Input {
23 filingStatus: FilingStatus;
24 };
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 '9': new ComputedLine((): number => {
78 // TODO - Itemized deductions.
79 switch (this.getInput('filingStatus')) {
80 case FilingStatus.Single:
81 case FilingStatus.MarriedFilingSeparate:
82 return 12200;
83 case FilingStatus.MarriedFilingJoint:
84 return 24400;
85 }
86 }, 'Deduction'),
87
88 '10': new ComputedLine((tr): number => {
89 const taxableIncome = this.getValue(tr, '8b');
90 let use8995a = false;
91 switch (this.getInput('filingStatus')) {
92 case FilingStatus.Single: use8995a = taxableIncome <= 160700; break;
93 case FilingStatus.MarriedFilingSeparate: use8995a = taxableIncome <= 160725; break;
94 case FilingStatus.MarriedFilingJoint: use8995a = taxableIncome <= 321400; break;
95 };
96 return 0;
97 }, 'Qualified business income deduction'),
98
99 '11a': new ComputedLine((tr): number => {
100 return this.getValue(tr, '9') + this.getValue(tr, '10');
101 }),
102 '11b': new ComputedLine((tr): number => {
103 const value = this.getValue(tr, '8b') - this.getValue(tr, '11a');
104 return value < 0 ? 0 : value;
105 }, 'Taxable income'),
106
107 '12a': new ComputedLine((tr): number => {
108 // Not supported:
109 // Form 8814 (election to report child's interest or dividends)
110 // Form 4972 (relating to lump-sum distributions)
111 const taxableIncome = this.getValue(tr, '11b');
112
113 if (this.getValue(tr, '3a') > 0 && !tr.findForm(ScheduleD))
114 throw new UnsupportedFeatureError('Qualified Dividends and Captial Gains Tax Worksheet not supported, Schedule D requried');
115
116 const schedD = tr.findForm(ScheduleDTaxWorksheet);
117 if (schedD)
118 return schedD.getValue(tr, '47');
119
120 return computeTax(taxableIncome, this.getInput('filingStatus'));
121 }, 'Tax'),
122
123 '12b': new ComputedLine((tr): number => {
124 return this.getValue(tr, '12a') + tr.getForm(Schedule2).getValue(tr, '3');
125 }, 'Additional tax'),
126
127 // Not supported: 13a - child tax credit
128
129 '13b': new ComputedLine((tr): number => {
130 // TODO: add Sched 3.L7
131 return 0;
132 }, 'Additional credits'),
133
134 '14': new ComputedLine((tr): number => {
135 const l12b = this.getValue(tr, '12b');
136 const l13b = this.getValue(tr, '13b');
137 const value = l12b - l13b;
138 return value < 0 ? 0 : value;
139 }),
140
141 '15': new ReferenceLine(Schedule2, '10', undefined, 0),
142
143 '16': new ComputedLine((tr): number => {
144 return this.getValue(tr, '14') + this.getValue(tr, '15');
145 }, 'Total tax'),
146
147 '17': new ComputedLine((tr): number => {
148 const fedTaxWithheldBoxes = [
149 new AccumulatorLine(FormW2, '2'),
150 new AccumulatorLine(Form1099R, '4'),
151 new AccumulatorLine(Form1099DIV, '4'),
152 new AccumulatorLine(Form1099INT, '4'),
153 ];
154 const withholding: number[] = fedTaxWithheldBoxes.map(b => b.value(tr));
155
156 let additionalMedicare = 0;
157 const f8959 = tr.findForm(Form8959)
158 if (f8959) {
159 additionalMedicare = f8959.getValue(tr, '24');
160 }
161
162 return reduceBySum(withholding) + additionalMedicare;
163 }, 'Federal income tax withheld'),
164
165 // 18 not supported
166
167 '19': new ReferenceLine(Form1040 as any, '17', 'Total payments'),
168
169 '20': new ComputedLine((tr): number => {
170 const l16: number = this.getValue(tr, '16');
171 const l19: number = this.getValue(tr, '19');
172 if (l19 > l16)
173 return l19 - l16;
174 return 0;
175 }, 'Amount overpaid'),
176
177 '23': new ComputedLine((tr): number => {
178 const l16 = this.getValue(tr, '16');
179 const l19 = this.getValue(tr, '19');
180 if (l19 < l16)
181 return l16 - l19;
182 return 0;
183 }, 'Amount you owe'),
184 };
185 };
186
187 export function computeTax(income: number, filingStatus: FilingStatus): number {
188 if (income < 100000)
189 throw new UnsupportedFeatureError('Tax-table tax liability not supported');
190
191 switch (filingStatus) {
192 case FilingStatus.Single:
193 if (income < 160725)
194 return (income * 0.24) - 5825.50;
195 else if (income < 204100)
196 return (income * 0.32) - 18683.50;
197 else if (income < 510300)
198 return (income * 0.35) - 24806.50;
199 else
200 return (income * 0.38) - 35012.50;
201 case FilingStatus.MarriedFilingJoint:
202 if (income < 168400)
203 return (income * 0.22) - 8283.00;
204 else if (income < 321450)
205 return (income * 0.24) - 11651.00;
206 else if (income < 408200)
207 return (income * 0.32) - 37367.00;
208 else if (income < 612350)
209 return (income * 0.35) - 49613.00;
210 else
211 return (income * 0.37) - 61860.00;
212 case FilingStatus.MarriedFilingSeparate:
213 if (income < 160725)
214 return (income * 0.24) - 5825.50;
215 else if (income < 204100)
216 return (income * 0.32) - 18683.50;
217 else if (income < 306175)
218 return (income * 0.35) - 24806.50;
219 else
220 return (income * 0.37) - 30930.00;
221 }
222 throw new UnsupportedFeatureError('Unexpected return type');
223 };