Implement the Schedule D Tax Worksheet.
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 23 Feb 2020 07:21:12 +0000 (02:21 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 23 Feb 2020 07:21:12 +0000 (02:21 -0500)
src/Math.test.ts [new file with mode: 0644]
src/Math.ts [new file with mode: 0644]
src/fed2019/Form1040.test.ts
src/fed2019/Form1040.ts
src/fed2019/ScheduleD.ts

diff --git a/src/Math.test.ts b/src/Math.test.ts
new file mode 100644 (file)
index 0000000..e6197c5
--- /dev/null
@@ -0,0 +1,7 @@
+import { clampToZero } from './Math';
+
+test('clamp to zero', () => {
+  expect(clampToZero(100)).toBe(100);
+  expect(clampToZero(-100)).toBe(0);
+  expect(clampToZero(0)).toBe(0);
+});
diff --git a/src/Math.ts b/src/Math.ts
new file mode 100644 (file)
index 0000000..5319278
--- /dev/null
@@ -0,0 +1,3 @@
+export function clampToZero(value: number): number {
+  return value < 0 ? 0 : value;
+}
index 6efea22e79d3834c3259e8f649751293c7da4ab0..d7cc234bdfb9c29e5b80ad06f6a97a77b023866f 100644 (file)
@@ -5,7 +5,7 @@ import Form1040, { FilingStatus, Schedule2 } from './Form1040';
 import Form1099DIV from './Form1099DIV';
 import Form1099INT from './Form1099INT';
 import Form1099B, { GainType } from './Form1099B';
-import ScheduleD from './ScheduleD';
+import ScheduleD, { ScheduleDTaxWorksheet } from './ScheduleD';
 import Form8959 from './Form8959';
 import Form8949 from './Form8949';
 import FormW2 from './FormW2';
@@ -83,6 +83,11 @@ test('capital gain/loss', () => {
   const p = Person.self('A');
   const tr = new TaxReturn(2019);
   tr.addForm(new Form1040({ filingStatus: FilingStatus.Single }));
+  tr.addForm(new FormW2({
+    employer: 'Money',
+    employee: p,
+    wages: 100000
+  }));
   tr.addForm(new Form1099B({
     payer: 'Brokerage',
     payee: p,
@@ -94,5 +99,7 @@ test('capital gain/loss', () => {
   }));
   tr.addForm(new Form8949);
   tr.addForm(new ScheduleD());
+  tr.addForm(new ScheduleDTaxWorksheet());
   tr.getForm(ScheduleD).getValue(tr, '21');
+  tr.getForm(Form1040).getValue(tr, '12a');
 });
index 273b3b85906d07290c4bb6c4fbfd89117dc7f6c5..f5822192c4ca4ba29b3652461413a7bb129bb044 100644 (file)
@@ -7,7 +7,7 @@ import Form8959 from './Form8959';
 import Form1099INT from './Form1099INT';
 import Form1099DIV from './Form1099DIV';
 import FormW2 from './FormW2';
-import ScheduleD from './ScheduleD';
+import ScheduleD, { ScheduleDTaxWorksheet } from './ScheduleD';
 
 export enum FilingStatus {
   Single,
@@ -93,43 +93,15 @@ export default class Form1040 extends Form<Form1040['_lines'], Form1040Input> {
       // Form 8814 (election to report child's interest or dividends)
       // Form 4972 (relating to lump-sum distributions)
       const taxableIncome = this.getValue(tr, '11b');
-      if (taxableIncome < 100000)
-        throw new UnsupportedFeatureError('Tax-table tax liability not supported');
 
-      const l11b = this.getValue(tr, '11b');
+      if (this.getValue(tr, '3a') > 0 && !tr.findForm(ScheduleD))
+        throw new UnsupportedFeatureError('Qualified Dividends and Captial Gains Tax Worksheet not supported, Schedule D requried');
 
-      switch (this.getInput('filingStatus')) {
-        case FilingStatus.Single:
-          if (taxableIncome < 160725)
-            return (l11b * 0.24) - 5825.50;
-          else if (taxableIncome < 204100)
-            return (l11b * 0.32) - 18683.50;
-          else if (taxableIncome < 510300)
-            return (l11b * 0.35) - 24806.50;
-          else
-            return (l11b * 0.38) - 35012.50;
-        case FilingStatus.MarriedFilingJoint:
-          if (taxableIncome < 168400)
-            return (l11b * 0.22) - 8283.00;
-          else if (taxableIncome < 321450)
-            return (l11b * 0.24) - 11651.00;
-          else if (taxableIncome < 408200)
-            return (l11b * 0.32) - 37367.00;
-          else if (taxableIncome < 612350)
-            return (l11b * 0.35) - 49613.00;
-          else
-            return (l11b * 0.37) - 61860.00;
-        case FilingStatus.MarriedFilingSeparate:
-          if (taxableIncome < 160725)
-            return (l11b * 0.24) - 5825.50;
-          else if (taxableIncome < 204100)
-            return (l11b * 0.32) - 18683.50;
-          else if (taxableIncome < 306175)
-            return (l11b * 0.35) - 24806.50;
-          else
-            return (l11b * 0.37) - 30930.00;
-      }
-      throw new UnsupportedFeatureError('Unexpected return type');
+      const schedD = tr.findForm(ScheduleDTaxWorksheet);
+      if (schedD)
+        return schedD.getValue(tr, '47');
+
+      return computeTax(taxableIncome, this.getInput('filingStatus'));
     }, 'Tax'),
 
     '12b': new ComputedLine((tr: TaxReturn): number => {
@@ -277,3 +249,41 @@ export class Schedule2 extends Form<Schedule2['_lines']> {
     })
   };
 };
+
+export function computeTax(income: number, filingStatus: FilingStatus): number {
+  if (income < 100000)
+    throw new UnsupportedFeatureError('Tax-table tax liability not supported');
+
+  switch (filingStatus) {
+    case FilingStatus.Single:
+      if (income < 160725)
+        return (income * 0.24) - 5825.50;
+      else if (income < 204100)
+        return (income * 0.32) - 18683.50;
+      else if (income < 510300)
+        return (income * 0.35) - 24806.50;
+      else
+        return (income * 0.38) - 35012.50;
+    case FilingStatus.MarriedFilingJoint:
+      if (income < 168400)
+        return (income * 0.22) - 8283.00;
+      else if (income < 321450)
+        return (income * 0.24) - 11651.00;
+      else if (income < 408200)
+        return (income * 0.32) - 37367.00;
+      else if (income < 612350)
+        return (income * 0.35) - 49613.00;
+      else
+        return (income * 0.37) - 61860.00;
+    case FilingStatus.MarriedFilingSeparate:
+      if (income < 160725)
+        return (income * 0.24) - 5825.50;
+      else if (income < 204100)
+        return (income * 0.32) - 18683.50;
+      else if (income < 306175)
+        return (income * 0.35) - 24806.50;
+      else
+        return (income * 0.37) - 30930.00;
+  }
+  throw new UnsupportedFeatureError('Unexpected return type');
+};
index 14ca67264daeb0c053b6c127e595c02e1a794a55..35f08aae54b8fb831769a636f13797f909bb8ee6 100644 (file)
@@ -1,11 +1,13 @@
 import Form from '../Form';
 import Person from '../Person';
 import TaxReturn from '../TaxReturn';
-import { Line, AccumulatorLine, ComputedLine, sumLineOfForms } from '../Line';
+import { Line, AccumulatorLine, ComputedLine, ReferenceLine, sumLineOfForms } from '../Line';
+import { clampToZero } from '../Math';
+import { UnsupportedFeatureError } from '../Errors';
 
 import Form8949, { Form8949Box } from './Form8949';
 import Form1099DIV from './Form1099DIV';
-import Form1040, { FilingStatus } from './Form1040';
+import Form1040, { FilingStatus, computeTax } from './Form1040';
 
 export default class ScheduleD extends Form<ScheduleD['_lines']> {
   readonly name = 'Schedule D';
@@ -52,16 +54,16 @@ export default class ScheduleD extends Form<ScheduleD['_lines']> {
     }, 'Both ST and LT are gains'),
 
     '18': new ComputedLine((tr: TaxReturn): number | undefined => {
-      if (!this.getValue(tr, '17'))
+      if (!this.getValue(tr, '17') || this.getValue(tr, '16') <= 0)
         return undefined;
-      // TODO
+      // Not supported - only for gains on Qualified Small Business Stock or collectibles.
       return 0;
     }, '28% Rate Gain Worksheet Value'),
 
     // 19 is not supported (Unrecaptured Section 1250 Gain Worksheet)
 
     '20': new ComputedLine((tr: TaxReturn): boolean | undefined => {
-      if (!this.getValue(tr, '17'))
+      if (!this.getValue(tr, '17') || this.getValue(tr, '16') <= 0)
         return undefined;
       const l18 = this.getValue(tr, '18');
       const l19 = undefined; //this.getValue(tr, '19');
@@ -69,7 +71,7 @@ export default class ScheduleD extends Form<ScheduleD['_lines']> {
     }, 'Line 18 and 19 both 0 or blank?'),
 
     '21': new ComputedLine((tr: TaxReturn): number | undefined => {
-      if (!this.getValue(tr, '17'))
+      if (!this.getValue(tr, '17') || !this.getValue(tr, '20'))
         return undefined;
       const filingStatus = tr.getForm(Form1040).getInput('filingStatus');
       const limit = filingStatus == FilingStatus.MarriedFilingSeparate ? -1500 : -3000;
@@ -77,3 +79,125 @@ export default class ScheduleD extends Form<ScheduleD['_lines']> {
     }, 'Net capital loss'),
   };
 };
+
+export class ScheduleDTaxWorksheet extends Form<ScheduleDTaxWorksheet['_lines']> {
+  readonly name = 'Schedule D Tax Worksheet';
+
+  protected readonly _lines = {
+    '1': new ReferenceLine(Form1040, '11b'),
+    '2': new ReferenceLine(Form1040, '3a'),
+    // TODO 3 - form 4952
+    // TODO 4 - 4952
+    '5': new ComputedLine((tr: TaxReturn): number => 0),
+    '6': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '2') - this.getValue(tr, '5'))),
+    '7': new ComputedLine((tr: TaxReturn): number => {
+      const schedD = tr.getForm(ScheduleD);
+      return Math.min(schedD.getValue(tr, '15'), schedD.getValue(tr, '16'));
+    }),
+    '8': new ComputedLine((tr: TaxReturn): number => {
+      return 0;
+      // return Math.min(this.getValue(tr, '3'), this.getValue(tr, '4'));
+    }),
+    '9': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '7') - this.getValue(tr, '8'))),
+    '10': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '6') + this.getValue(tr, '9')),
+    '11': new ComputedLine((tr: TaxReturn): number => {
+      const schedD = tr.getForm(ScheduleD);
+      // TODO - line 19 is not supported.
+      return Math.min(schedD.getValue(tr, '18'), Infinity); //schedD.getValue(tr, '19'));
+    }),
+    '12': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '9'), this.getValue(tr, '11'))),
+    '13': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '10') - this.getValue(tr, '12')),
+    '14': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '13'))),
+    '15': new ComputedLine((tr: TaxReturn): number => {
+      switch (tr.getForm(Form1040).getInput('filingStatus')) {
+        case FilingStatus.Single:
+        case FilingStatus.MarriedFilingSeparate:
+          return 39375;
+        case FilingStatus.MarriedFilingJoint:
+          return 78750;
+      }
+    }),
+    '16': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '15'))),
+    '17': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '16'))),
+    '18': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '10'))),
+    '19': new ComputedLine((tr: TaxReturn): number => {
+      let threshold: number;
+      switch (tr.getForm(Form1040).getInput('filingStatus')) {
+        case FilingStatus.Single:
+        case FilingStatus.MarriedFilingSeparate:
+          threshold = 160725;
+          break;
+        case FilingStatus.MarriedFilingJoint:
+          threshold = 321450;
+          break;
+      }
+      return Math.min(this.getValue(tr, '1'), threshold);
+    }),
+    '20': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '19'))),
+    '21': new ComputedLine((tr: TaxReturn): number => Math.max(this.getValue(tr, '18'), this.getValue(tr, '20'))),
+    '22': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '16') - this.getValue(tr, '17')),
+    '23': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '13'))),
+    '24': new ReferenceLine(ScheduleDTaxWorksheet as any, '22'),
+    '25': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '23') - this.getValue(tr, '24'))),
+    '26': new ComputedLine((tr: TaxReturn): number => {
+      switch (tr.getForm(Form1040).getInput('filingStatus')) {
+        case FilingStatus.Single:
+          return 434550;
+        case FilingStatus.MarriedFilingSeparate:
+          return 244425;
+        case FilingStatus.MarriedFilingJoint:
+          return 488850;
+      }
+    }),
+    '27': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '26'))),
+    '28': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '21') + this.getValue(tr, '22')),
+    '29': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '27') - this.getValue(tr, '28'))),
+    '30': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '25'), this.getValue(tr, '29'))),
+    '31': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '30') * 0.15),
+    '32': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '24') + this.getValue(tr, '30')),
+    '33': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '23') - this.getValue(tr, '32')),
+    '34': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '33') * 0.20),
+    '35': new ComputedLine((tr: TaxReturn): number => {
+      const schedD = tr.getForm(ScheduleD);
+      // TODO - line 19 is not supported.
+      return Math.min(this.getValue(tr, '9'), Infinity); //schedD.getValue(tr, '19'));
+    }),
+    '36': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '10') + this.getValue(tr, '21')),
+    '37': new ReferenceLine(ScheduleDTaxWorksheet as any, '1'),
+    '38': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '36') - this.getValue(tr, '37'))),
+    '39': new ComputedLine((tr: TaxReturn): number => clampToZero(this.getValue(tr, '35') - this.getValue(tr, '38'))),
+    '40': new ComputedLine((tr: TaxReturn): number => this.getValue(tr, '39') * 0.25),
+    '41': new ComputedLine((tr: TaxReturn): number => {
+      const schedD = tr.getForm(ScheduleD);
+      if (schedD.getValue(tr, '18'))
+        throw new UnsupportedFeatureError('28% Gain unsupported');
+      return 0;
+    }),
+    '42': new ComputedLine((tr: TaxReturn): number => {
+      if (!tr.getForm(ScheduleD).getValue(tr, '18'))
+        return 0;
+      return this.getValue(tr, '1') - this.getValue(tr, '41');
+    }),
+    '43': new ComputedLine((tr: TaxReturn): number => {
+      if (!tr.getForm(ScheduleD).getValue(tr, '18'))
+        return 0;
+      return this.getValue(tr, '42') * 0.28;
+    }),
+    '44': new ComputedLine((tr: TaxReturn): number => {
+      const income = this.getValue(tr, '21');
+      return computeTax(income, tr.getForm(Form1040).getInput('filingStatus'));
+    }),
+    '45': new ComputedLine((tr: TaxReturn): number => {
+      return this.getValue(tr, '31') +
+             this.getValue(tr, '34') +
+             this.getValue(tr, '40') +
+             this.getValue(tr, '43') +
+             this.getValue(tr, '44');
+    }),
+    '46': new ComputedLine((tr: TaxReturn): number => {
+      const income = this.getValue(tr, '1');
+      return computeTax(income, tr.getForm(Form1040).getInput('filingStatus'));
+    }),
+    '47': new ComputedLine((tr: TaxReturn): number => Math.min(this.getValue(tr, '45'), this.getValue(tr, '46'))),
+  };
+};