From 4fc4f0f4d80b12c2a9b6c2c32a79843963dd0f85 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 23 Feb 2020 02:21:12 -0500 Subject: [PATCH] Implement the Schedule D Tax Worksheet. --- src/Math.test.ts | 7 ++ src/Math.ts | 3 + src/fed2019/Form1040.test.ts | 9 ++- src/fed2019/Form1040.ts | 82 +++++++++++---------- src/fed2019/ScheduleD.ts | 136 +++++++++++++++++++++++++++++++++-- 5 files changed, 194 insertions(+), 43 deletions(-) create mode 100644 src/Math.test.ts create mode 100644 src/Math.ts diff --git a/src/Math.test.ts b/src/Math.test.ts new file mode 100644 index 0000000..e6197c5 --- /dev/null +++ b/src/Math.test.ts @@ -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 index 0000000..5319278 --- /dev/null +++ b/src/Math.ts @@ -0,0 +1,3 @@ +export function clampToZero(value: number): number { + return value < 0 ? 0 : value; +} diff --git a/src/fed2019/Form1040.test.ts b/src/fed2019/Form1040.test.ts index 6efea22..d7cc234 100644 --- a/src/fed2019/Form1040.test.ts +++ b/src/fed2019/Form1040.test.ts @@ -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'); }); diff --git a/src/fed2019/Form1040.ts b/src/fed2019/Form1040.ts index 273b3b8..f582219 100644 --- a/src/fed2019/Form1040.ts +++ b/src/fed2019/Form1040.ts @@ -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 { // 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 { }) }; }; + +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'); +}; diff --git a/src/fed2019/ScheduleD.ts b/src/fed2019/ScheduleD.ts index 14ca672..35f08aa 100644 --- a/src/fed2019/ScheduleD.ts +++ b/src/fed2019/ScheduleD.ts @@ -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 { readonly name = 'Schedule D'; @@ -52,16 +54,16 @@ export default class ScheduleD extends Form { }, '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 { }, '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 { }, 'Net capital loss'), }; }; + +export class ScheduleDTaxWorksheet extends Form { + 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'))), + }; +}; -- 2.22.5