Add Form 8960.
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 8 Mar 2020 17:20:18 +0000 (13:20 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 8 Mar 2020 17:20:18 +0000 (13:20 -0400)
src/fed2019/Form8960.test.ts [new file with mode: 0644]
src/fed2019/Form8960.ts [new file with mode: 0644]
src/fed2019/Schedule2.ts

diff --git a/src/fed2019/Form8960.test.ts b/src/fed2019/Form8960.test.ts
new file mode 100644 (file)
index 0000000..d77ceba
--- /dev/null
@@ -0,0 +1,128 @@
+import Person from '../Person';
+import TaxReturn from '../TaxReturn';
+
+import FormW2 from './FormW2';
+import Form1040, { FilingStatus } from './Form1040';
+import Form1099B, { GainType } from './Form1099B';
+import Form1099DIV from './Form1099DIV';
+import Form1099INT from './Form1099INT';
+import Form8949 from './Form8949';
+import Form8959 from './Form8959';
+import Form8960 from './Form8960';
+import Schedule2 from './Schedule2';
+import ScheduleD from './ScheduleD';
+
+describe('net investment income tax', () => {
+  const filingStatusToResult = {
+    [FilingStatus.Single]: 105555,
+    [FilingStatus.MarriedFilingJoint]: 55555,
+    [FilingStatus.MarriedFilingSeparate]: 180555,
+  };
+
+  for (const filingStatus of Object.values(FilingStatus)) {
+    test(`filing status ${filingStatus}`, () => {
+      const p = Person.self('A');
+      const tr = new TaxReturn(2019);
+      tr.addForm(new Form1040({ filingStatus }));
+      tr.addForm(new Form1099DIV({
+        payer: 'Brokerage',
+        payee: p,
+        ordinaryDividends: 2000,
+        qualifiedDividends: 1500,
+        totalCapitalGain: 55
+      }));
+      tr.addForm(new Form1099INT({
+        payer: 'Bank',
+        payee: p,
+        interest: 3000
+      }));
+      tr.addForm(new Form1099B({
+        payer: 'Brokerage',
+        payee: p,
+        description: '100 VTI',
+        proceeds: 4000,
+        costBasis: 3500,
+        gainType: GainType.LongTerm,
+        basisReportedToIRS: true
+      }));
+      tr.addForm(new Form8949);
+      tr.addForm(new ScheduleD);
+      tr.addForm(new FormW2({
+        employer: 'Acme',
+        employee: p,
+        wages: 300000,
+        fedIncomeTax: 0,
+        medicareWages: 0,
+        medicareTax: 0,
+      }));
+      tr.addForm(new Form8959);
+      tr.addForm(new Schedule2);
+
+      const f = new Form8960();
+      tr.addForm(f);
+
+      expect(f.getValue(tr, '1')).toBe(3000);
+      expect(f.getValue(tr, '2')).toBe(2000);
+      expect(f.getValue(tr, '5a')).toBe(555);
+      expect(f.getValue(tr, '8')).toBe(5555);
+      expect(f.getValue(tr, '11')).toBe(0);
+      expect(f.getValue(tr, '12')).toBe(5555);
+      expect(f.getValue(tr, '13')).toBe(305555);
+      expect(f.getValue(tr, '14')).toBe(Form8960.filingStatusLimit(filingStatus));
+      expect(f.getValue(tr, '15')).toBe(filingStatusToResult[filingStatus]);
+      expect(f.getValue(tr, '16')).toBe(5555);
+      expect(f.getValue(tr, '17')).toBe(5555 * 0.038);
+
+      expect(tr.getForm(Schedule2).getValue(tr, '8')).toBe(5555 * 0.038);
+    });
+  }
+});
+
+describe('no net investment income tax', () => {
+  for (const filingStatus of Object.values(FilingStatus)) {
+    test(`filing status ${filingStatus}`, () => {
+      const p = Person.self('A');
+      const tr = new TaxReturn(2019);
+      tr.addForm(new Form1040({ filingStatus }));
+      tr.addForm(new Form1099DIV({
+        payer: 'Brokerage',
+        payee: p,
+        ordinaryDividends: 2000,
+        qualifiedDividends: 1500,
+        totalCapitalGain: 55
+      }));
+      tr.addForm(new Form1099INT({
+        payer: 'Bank',
+        payee: p,
+        interest: 3000
+      }));
+      tr.addForm(new Form1099B({
+        payer: 'Brokerage',
+        payee: p,
+        description: '100 VTI',
+        proceeds: 4000,
+        costBasis: 3500,
+        gainType: GainType.LongTerm,
+        basisReportedToIRS: true
+      }));
+      tr.addForm(new Form8949);
+      tr.addForm(new ScheduleD);
+      tr.addForm(new FormW2({
+        employer: 'Acme',
+        employee: p,
+        wages: 70000,
+        fedIncomeTax: 0,
+        medicareWages: 0,
+        medicareTax: 0,
+      }));
+      tr.addForm(new Form8959);
+      tr.addForm(new Schedule2);
+
+      const f = new Form8960();
+      tr.addForm(f);
+
+      expect(f.getValue(tr, '17')).toBe(0);
+      expect(tr.getForm(Schedule2).getValue(tr, '8')).toBe(0);
+    });
+  }
+});
diff --git a/src/fed2019/Form8960.ts b/src/fed2019/Form8960.ts
new file mode 100644 (file)
index 0000000..9d73220
--- /dev/null
@@ -0,0 +1,71 @@
+import Form from '../Form';
+import TaxReturn from '../TaxReturn';
+import { ComputedLine, ReferenceLine } from '../Line';
+import { clampToZero } from '../Math';
+
+import Form1040, { FilingStatus } from './Form1040';
+import Schedule1 from './Schedule1';
+
+export default class Form8960 extends Form<Form8960['_lines']> {
+  readonly name = '8960';
+
+  protected readonly _lines = {
+    // Part 1
+    // Section 6013 elections not supported.
+    '1': new ReferenceLine(Form1040, '2b', 'Taxable interest'),
+    '2': new ReferenceLine(Form1040, '3b', 'Ordinary dividends'),
+    // 3 not supported - Annuities
+    // 4a not supported - Rental real estate, royalties, partnerships, S corporations, trusts, etc
+    // 4b not supported - Adjustment for net income or loss derived in the ordinary course of a nonsection 1411 trade or business 
+    // 4c not supported - 4a+4b
+    '5a': new ComputedLine((tr): number => {
+      return (new ReferenceLine(Form1040, '6')).value(tr) +
+             (new ReferenceLine(Schedule1, '4', undefined, 0)).value(tr);
+    }, 'Net gain or loss'),
+    // 5b not supported - Net gain or loss from disposition of property that is not subject to net investment income tax
+    // 5c not supported - Adjustment from disposition of partnership interest or S corporation stock
+    '5d': new ComputedLine((tr): number => {
+      // Should include 5b-5c.
+      return this.getValue(tr, '5a');
+    }),
+    // 6 not supported - Adjustments to investment income for certain CFCs and PFICs
+    // 7 not supported - Other modifications to investment income
+    '8': new ComputedLine((tr): number => {
+      return this.getValue(tr, '1') +
+             this.getValue(tr, '2') +
+             /*this.getValue(tr, '3') +
+             this.getValue(tr, '4c') +*/
+             this.getValue(tr, '5d') /*+
+             this.getValue(tr, '6') +
+             this.getValue(tr, '7')*/;
+    }),
+
+    // Part 2
+    // 9a not supported - Investment interest expenses
+    // 9b not supported - State, local, and foreign income tax
+    // 9c not supported - Miscellaneous investment expenses
+    // 9d not supported - 9a+9b+9c
+    // 10 not supported - Additional modifications
+    '11': new ComputedLine(() => 0, 'Total deductions and modifications'),  // Not supported - 9d+10.
+
+    // Part 3
+    '12': new ComputedLine((tr): number => this.getValue(tr, '8') - this.getValue(tr, '11'), 'Net investment income'),
+    '13': new ReferenceLine(Form1040, '8b', 'Modified adjusted gross income'),
+    '14': new ComputedLine((tr): number => {
+      return Form8960.filingStatusLimit(tr.getForm(Form1040).getInput('filingStatus'));
+    }, 'Threshold'),
+    '15': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '13') - this.getValue(tr, '14'))),
+    '16': new ComputedLine((tr): number => Math.min(this.getValue(tr, '12'), this.getValue(tr, '15'))),
+    '17': new ComputedLine((tr): number => this.getValue(tr, '16') * 0.038, 'Net investment income tax'),
+
+    // 18 - 21 not supported (Estates and Trusts)
+  };
+
+  static filingStatusLimit(filingStatus: FilingStatus): number {
+    switch (filingStatus) {
+      case FilingStatus.MarriedFilingJoint:    return 250000;
+      case FilingStatus.MarriedFilingSeparate: return 125000;
+      case FilingStatus.Single:                return 200000;
+    }
+  }
+};
index aed340613cc7791ec12d84a9a51b5c2210df7da9..58863b4d402d61f15f168f9202e02ef63d65c0b1 100644 (file)
@@ -4,7 +4,10 @@ import { ComputedLine } from '../Line';
 import { UnsupportedFeatureError } from '../Errors';
 
 import Form1040, { FilingStatus } from './Form1040';
+import Form1099DIV from './Form1099DIV';
+import Form1099INT from './Form1099INT';
 import Form8959 from './Form8959';
+import Form8960 from './Form8960';
 
 export default class Schedule2 extends Form<Schedule2['_lines']> {
   readonly name = 'Schedule 2';
@@ -41,39 +44,19 @@ export default class Schedule2 extends Form<Schedule2['_lines']> {
     '8': new ComputedLine((tr): number => {
       const f1040 = tr.getForm(Form1040);
       const wages = f1040.getLine('1').value(tr);
-
-      let niit: boolean;
       const filingStatus = f1040.getInput('filingStatus');
 
-      const additionalMedicare = wages > Form8959.filingStatusLimit(filingStatus);
-
-      switch (f1040.getInput('filingStatus')) {
-        case FilingStatus.Single:
-          if (wages > 200000) {
-            niit = true;
-          }
-          break;
-        case FilingStatus.MarriedFilingJoint:
-          if (wages > 250000) {
-            niit = true;
-          }
-          break;
-        case FilingStatus.MarriedFilingSeparate:
-          if (wages > 125000) {
-            niit = true;
-          }
-          break;
-      }
-
       let value = 0;
 
-      if (additionalMedicare) {
-        const f8959 = tr.getForm(Form8959);
-        value += f8959.getValue(tr, '18');
+      // Additional medicare tax.
+      if (wages > Form8959.filingStatusLimit(filingStatus)) {
+        value += tr.getForm(Form8959).getValue(tr, '18');
       }
 
-      if (niit) {
-        //const f8960 = tr.getForm('8960');
+      // Net investment income tax.
+      if (wages > Form8960.filingStatusLimit(filingStatus) &&
+          (tr.findForms(Form1099DIV).length || tr.findForms(Form1099INT).length)) {
+        value += tr.getForm(Form8960).getValue(tr, '17');
       }
 
       return value;
@@ -86,4 +69,3 @@ export default class Schedule2 extends Form<Schedule2['_lines']> {
     })
   };
 };
-