From 4541512c1c88251c752392f7de3c543a281a3c00 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 8 Mar 2020 01:08:29 -0500 Subject: [PATCH] Add Form 1116. --- src/fed2019/Form1040.test.ts | 2 +- src/fed2019/Form1040.ts | 14 +++- src/fed2019/Form1116.test.ts | 129 +++++++++++++++++++++++++++++++++++ src/fed2019/Form1116.ts | 113 ++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 src/fed2019/Form1116.test.ts create mode 100644 src/fed2019/Form1116.ts diff --git a/src/fed2019/Form1040.test.ts b/src/fed2019/Form1040.test.ts index 3c9aa03..8523a38 100644 --- a/src/fed2019/Form1040.test.ts +++ b/src/fed2019/Form1040.test.ts @@ -88,7 +88,7 @@ test('capital gain/loss', () => { tr.addForm(new FormW2({ employer: 'Money', employee: p, - wages: 100000 + wages: 150000 })); tr.addForm(new Form1099B({ payer: 'Brokerage', diff --git a/src/fed2019/Form1040.ts b/src/fed2019/Form1040.ts index 2aed67f..4d9fe8e 100644 --- a/src/fed2019/Form1040.ts +++ b/src/fed2019/Form1040.ts @@ -73,8 +73,16 @@ export default class Form1040 extends Form { return this.getValue(tr, '7b') - this.getValue(tr, '8a'); }, 'Adjusted gross income'), - // TODO - Deduction - '9': new ComputedLine(() => 0, 'Deduction'), + '9': new ComputedLine((): number => { + // TODO - Itemized deductions. + switch (this.getInput('filingStatus')) { + case FilingStatus.Single: + case FilingStatus.MarriedFilingSeparate: + return 12200; + case FilingStatus.MarriedFilingJoint: + return 24400; + } + }, 'Deduction'), '10': new ComputedLine((tr): number => { const taxableIncome = this.getValue(tr, '8b'); @@ -197,7 +205,7 @@ export class Schedule2 extends Form { } throw new UnsupportedFeatureError('The AMT is not supported'); }, 'AMT'), - // 2 is not supported (Excess advance premium tax credit repayment) + '2': new ComputedLine(() => 0, 'Excess advance premium tax credit repayment'), // Not supported. '3': new ComputedLine((tr): number => { // Should include line 2. return this.getValue(tr, '1'); diff --git a/src/fed2019/Form1116.test.ts b/src/fed2019/Form1116.test.ts new file mode 100644 index 0000000..99ada28 --- /dev/null +++ b/src/fed2019/Form1116.test.ts @@ -0,0 +1,129 @@ +import TaxReturn from '../TaxReturn'; +import Person from '../Person'; +import { UnsupportedFeatureError } from '../Errors'; + +import Form1040, { FilingStatus } from './Form1040'; +import Form1116, { ForeignIncomeCategory } from './Form1116'; +import Form1099B, { GainType } from './Form1099B'; +import Form1099DIV from './Form1099DIV'; +import Form8949 from './Form8949'; +import FormW2 from './FormW2'; +import ScheduleD from './ScheduleD'; + +test('supported income category', () => { + const p = Person.self('A'); + const tr = new TaxReturn(2019); + const f = new Form1116({ + person: p, + incomeCategory: ForeignIncomeCategory.C, + posessionName: "RIC", + grossForeignIncome: 100, + totalForeignTaxesPaidOrAccrued: 0 + }); + tr.addForm(f); + expect(f.getValue(tr, 'category')).toBe(ForeignIncomeCategory.C); +}); + +test('unsupported income categories', () => { + for (const category of Object.values(ForeignIncomeCategory)) { + if (category == ForeignIncomeCategory.C) + continue; + + const p = Person.self('B'); + const tr = new TaxReturn(2019); + const f = new Form1116({ + person: p, + incomeCategory: category, + posessionName: "RIC", + grossForeignIncome: 100, + totalForeignTaxesPaidOrAccrued: 0 + }); + tr.addForm(f); + expect(() => f.getValue(tr, 'category')).toThrow(UnsupportedFeatureError); + } +}); + +test('foreign tax credit', () => { + const p = Person.self('A'); + const tr = new TaxReturn(2019); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint + })); + tr.addForm(new FormW2({ + employer: 'ACME', + employee: p, + wages: 697000, + })); + + const f = new Form1116({ + person: p, + incomeCategory: ForeignIncomeCategory.C, + posessionName: "RIC", + grossForeignIncome: 99, + totalForeignTaxesPaidOrAccrued: 14 + }); + tr.addForm(f); + + expect(f.getValue(tr, '1a')).toBe(99); + expect(f.getValue(tr, '3a')).toBe(24400); + expect(f.getValue(tr, '3c')).toBe(24400); + expect(f.getValue(tr, '3d')).toBe(99); + expect(f.getValue(tr, '3e')).toBe(697000); + expect(f.getValue(tr, '3f')).toBe(0.0001); + expect(f.getValue(tr, '3g')).toBeCloseTo(2.44); + expect(f.getValue(tr, '6')).toBeCloseTo(2.44); + expect(f.getValue(tr, '7')).toBeCloseTo(96.56); + expect(f.getValue(tr, '8')).toBe(14); + expect(f.getValue(tr, '9')).toBe(14); + expect(f.getValue(tr, '14')).toBe(14); + expect(f.getValue(tr, '20')).toBe(((697000-24400) * 0.37) - 61860); + expect(f.getValue(tr, '21')).toBeCloseTo(26.846); + expect(f.getValue(tr, '22')).toBe(14); + expect(f.getValue(tr, '31')).toBe(14); + expect(f.getValue(tr, '33')).toBe(14); +}); + +test('no net capital losses in total income', () => { + const p = Person.self('A'); + const tr = new TaxReturn(2019); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint + })); + tr.addForm(new FormW2({ + employer: 'Megacorp', + employee: p, + wages: 200000 + })); + tr.addForm(new Form1099B({ + payer: 'Brokerage', + payee: p, + description: 'SCHF', + proceeds: 100, + costBasis: 50, + gainType: GainType.LongTerm, + basisReportedToIRS: true + })); + tr.addForm(new Form1099B({ + payer: 'Brokerage', + payee: p, + description: 'SCHE', + proceeds: 60, + costBasis: 100, + gainType: GainType.ShortTerm, + basisReportedToIRS: true + })); + tr.addForm(new Form8949); + tr.addForm(new ScheduleD); + + const f = new Form1116({ + person: p, + incomeCategory: ForeignIncomeCategory.C, + posessionName: 'RIC', + grossForeignIncome: 200, + totalForeignTaxesPaidOrAccrued: 65 + }); + + expect(tr.getForm(Form8949).getValue(tr, 'boxA').gainOrLoss).toBe(-40); + expect(tr.getForm(Form8949).getValue(tr, 'boxD').gainOrLoss).toBe(50); + expect(f.getValue(tr, '3e')).toBe(200050); +}); diff --git a/src/fed2019/Form1116.ts b/src/fed2019/Form1116.ts new file mode 100644 index 0000000..769e18d --- /dev/null +++ b/src/fed2019/Form1116.ts @@ -0,0 +1,113 @@ +import Form from '../Form'; +import TaxReturn from '../TaxReturn'; +import Person from '../Person'; +import { ComputedLine, InputLine, ReferenceLine } from '../Line'; +import { UnsupportedFeatureError } from '../Errors'; +import { reduceBySum } from '../Math'; + +import Form1040, { Schedule2 } from './Form1040'; +import Form8949 from './Form8949'; +import ScheduleD from './ScheduleD'; + +export enum ForeignIncomeCategory { + A = 'A: Section 951A category', + B = 'B: Foreign branch category', + C = 'C: Passive category', + D = 'D: General category', + E = 'E: Section 901(j)', + F = 'F: Certain income re-sourced by treaty', + G = 'G: Lump-sum distributions', +}; + +export interface Form1116Input { + person: Person; + incomeCategory: ForeignIncomeCategory; + posessionName: 'RIC' | string; + grossForeignIncome: number; + lossesFromForeignSources?: number; + totalForeignTaxesPaidOrAccrued: number; +}; + +class Input extends InputLine {}; + +export default class Form1116 extends Form { + readonly name = '1116'; + + protected readonly _lines = { + 'category': new ComputedLine((tr: TaxReturn): ForeignIncomeCategory => { + const input = this.getInput('incomeCategory'); + if (input != ForeignIncomeCategory.C) + throw new UnsupportedFeatureError(`Form 1116 does not support ${input}`); + return input; + }), + 'i': new Input('posessionName'), + '1a': new Input('grossForeignIncome'), + // 1b not supported - services as an employee. + // 2 not supported - Expenses definitely related to the income + '3a': new ReferenceLine(Form1040, '9', 'Deductions'), + '3b': new ComputedLine(() => 0, 'Other deductions'), // Not supported + '3c': new ComputedLine((tr): number => { + return this.getValue(tr, '3a') + this.getValue(tr, '3b'); + }), + '3d': new ReferenceLine(Form1116 as any, '1a'), // Should exclude income from unsupported Form 2555. + '3e': new ComputedLine((tr): number => { + const f1040 = tr.findForm(Form1040); + // Take total income, but do not net capital gains out with losses, so remove + // line 6. + let grossIncome = f1040.getValue(tr, '7b') - f1040.getValue(tr, '6'); + const f8949 = tr.findForm(Form8949); + if (f8949) { + const keys: (keyof Form8949['lines'])[] = ['boxA', 'boxB', 'boxC', 'boxD', 'boxE', 'boxF']; + const values = keys.map(k => f8949.getValue(tr, k).gainOrLoss).filter(n => n > 0); + grossIncome += reduceBySum(values); + + grossIncome += tr.getForm(ScheduleD).getValue(tr, '13'); + } + return grossIncome; + }), + '3f': new ComputedLine((tr): number => { + return Number.parseFloat((this.getValue(tr, '3d') / this.getValue(tr, '3e')).toFixed(4)); + }), + '3g': new ComputedLine((tr): number => { + return this.getValue(tr, '3c') * this.getValue(tr, '3f'); + }), + // 4 not supported - Pro rata share of interest expense + '5': new Input('lossesFromForeignSources', undefined, 0), + '6': new ComputedLine((tr): number => { + // Should include 2, 4a, 4b. + return this.getValue(tr, '3g') + this.getValue(tr, '5'); + }), + '7': new ComputedLine((tr): number => this.getValue(tr, '1a') - this.getValue(tr, '6')), + // Skip the complicated Part II matrix and just use the input value. + '8': new Input('totalForeignTaxesPaidOrAccrued'), + '9': new ReferenceLine(Form1116 as any, '8'), + // 10 not supported - Carryback or carryover + '11': new ComputedLine((tr): number => this.getValue(tr, '9') /* + this.getValue(tr, '10') */), + // 12 not supported - Reduction in foreign taxes + // 13 not supported - Taxes reclassified under high tax kickout + '14': new ComputedLine((tr): number => { + return this.getValue(tr, '11') /*+ + this.getValue(tr, '12') + + this.getValue(tr, '13')*/; + }), + '15': new ReferenceLine(Form1116 as any, '7'), + // 16 not supported - Adjustments to line 15 + '17': new ComputedLine((tr): number => this.getValue(tr, '15') /* + this.getValue(tr, '16') */), + // TODO - This does not handle necessary adjustments. + '18': new ReferenceLine(Form1040, '11b'), + '19': new ComputedLine((tr): number => this.getValue(tr, '17') / this.getValue(tr, '18')), + '20': new ComputedLine((tr): number => { + let value = tr.getForm(Form1040).getValue(tr, '12a'); + const sched2 = tr.findForm(Schedule2); + if (sched2) + value += sched2.getValue(tr, '2'); + return value; + }), + '21': new ComputedLine((tr): number => this.getValue(tr, '20') * this.getValue(tr, '19'), 'Maximum amount of credit'), + '22': new ComputedLine((tr): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '21'))), + // 23-30 not supported (other category F1116) + '31': new ReferenceLine(Form1116 as any, '22'), + // 32 not supported - Reduction of credit for international boycott operations + '33': new ComputedLine((tr): number => this.getValue(tr, '31') /* - this.getValue(tr, '32')*/), + }; +}; -- 2.22.5