Add Form 1116.
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 8 Mar 2020 06:08:29 +0000 (01:08 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 8 Mar 2020 06:08:29 +0000 (01:08 -0500)
src/fed2019/Form1040.test.ts
src/fed2019/Form1040.ts
src/fed2019/Form1116.test.ts [new file with mode: 0644]
src/fed2019/Form1116.ts [new file with mode: 0644]

index 3c9aa03c50d1ab39158cb79c9050b86a0bd4f64b..8523a38b1b1e16427ffb09d4ed84a49862dd6a46 100644 (file)
@@ -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',
index 2aed67f7cca5c5b40623eb579c3ef83df9213a13..4d9fe8e942b6bc35f42245f11668abfa348026a4 100644 (file)
@@ -73,8 +73,16 @@ export default class Form1040 extends Form<Form1040['_lines'], Form1040Input> {
       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<Schedule2['_lines']> {
       }
       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 (file)
index 0000000..99ada28
--- /dev/null
@@ -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 (file)
index 0000000..769e18d
--- /dev/null
@@ -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<T extends keyof Form1116Input> extends InputLine<Form1116Input, T> {};
+
+export default class Form1116 extends Form<Form1116['_lines'], Form1116Input> {
+  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')*/),
+  };
+};