From 595c47fabbcec4e614e6ea4c8f24621afd9ab5c9 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 13 Jul 2020 16:28:41 -0400 Subject: [PATCH] Add Schedule A for itemized deductions. --- src/fed2019/Form1040.test.ts | 28 +++++++ src/fed2019/Form1040.ts | 17 +++- src/fed2019/README.md | 1 + src/fed2019/ScheduleA.test.ts | 142 ++++++++++++++++++++++++++++++++++ src/fed2019/ScheduleA.ts | 81 +++++++++++++++++++ src/fed2019/index.ts | 1 + 6 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 src/fed2019/ScheduleA.test.ts create mode 100644 src/fed2019/ScheduleA.ts diff --git a/src/fed2019/Form1040.test.ts b/src/fed2019/Form1040.test.ts index 109b74f..152dd6c 100644 --- a/src/fed2019/Form1040.test.ts +++ b/src/fed2019/Form1040.test.ts @@ -12,6 +12,7 @@ import Form1099INT from './Form1099INT'; import Form1099B from './Form1099B'; import Form1099R, { Box7Code } from './Form1099R'; import Schedule2 from './Schedule2'; +import ScheduleA from './ScheduleA'; import ScheduleD, { ScheduleDTaxWorksheet } from './ScheduleD'; import Form8606 from './Form8606'; import Form8959 from './Form8959'; @@ -206,3 +207,30 @@ test('backdoor and megabackdoor roth', () => { expect(f.getValue(tr, '4a')).toBe(6000); expect(f.getValue(tr, '4b')).toBe(0); }); + +test('itemized deductions', () => { + const tr = new TaxReturn(); + const f = new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint + }); + tr.addForm(f); + tr.addForm(new ScheduleA({ + charitableGiftsCashOrCheck: 26000 + })); + + expect(f.getValue(tr, '9')).toBe(26000); +}); + +test('itemized deductions, forced', () => { + const tr = new TaxReturn(); + const f = new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint + }); + tr.addForm(f); + tr.addForm(new ScheduleA({ + stateAndLocalIncomeAndSalesTaxes: 10000, + forceItemize: true + })); + + expect(f.getValue(tr, '9')).toBe(10000); +}); diff --git a/src/fed2019/Form1040.ts b/src/fed2019/Form1040.ts index 4a8b2d0..579e8eb 100644 --- a/src/fed2019/Form1040.ts +++ b/src/fed2019/Form1040.ts @@ -18,6 +18,7 @@ import W2 from './W2'; import Schedule1 from './Schedule1'; import Schedule2 from './Schedule2'; import Schedule3 from './Schedule3'; +import ScheduleA from './ScheduleA'; import ScheduleD, { ScheduleDTaxWorksheet } from './ScheduleD'; export enum FilingStatus { @@ -81,14 +82,22 @@ export default class Form1040 extends Form { return this.getValue(tr, '7b') - this.getValue(tr, '8a'); }, 'Adjusted gross income'), - '9': new ComputedLine((): number => { - // TODO - Itemized deductions. + '9': new ComputedLine((tr): number => { + let deduction = 0; + const schedA = tr.findForm(ScheduleA); + if (schedA) { + deduction = schedA.getValue(tr, '17'); + if (schedA.getValue(tr, '18')) { + return deduction; + } + } + switch (this.filingStatus) { case FilingStatus.Single: case FilingStatus.MarriedFilingSeparate: - return 12200; + return Math.max(deduction, 12200); case FilingStatus.MarriedFilingJoint: - return 24400; + return Math.max(deduction, 24400); } }, 'Deduction'), diff --git a/src/fed2019/README.md b/src/fed2019/README.md index b0a46ff..01c186c 100644 --- a/src/fed2019/README.md +++ b/src/fed2019/README.md @@ -20,6 +20,7 @@ The following forms are at least partially supported: - **Schedule 3:** Additional credits and payments - **Schedule B:** _This form is not directly modeled, and instead the computations are done on 1040._ +- **Schedule A:** Itemized deductions - **Schedule D:** Capital gains and losses - **Form 1099-B:** Proceeds from broker transactions - **Form 1099-DIV:** Dividend income diff --git a/src/fed2019/ScheduleA.test.ts b/src/fed2019/ScheduleA.test.ts new file mode 100644 index 0000000..53ad2d5 --- /dev/null +++ b/src/fed2019/ScheduleA.test.ts @@ -0,0 +1,142 @@ +// Copyright 2020 Blue Static +// This program is free software licensed under the GNU General Public License, +// version 3.0. The full text of the license can be found in LICENSE.txt. +// SPDX-License-Identifier: GPL-3.0-only + +import { Person } from '../core'; +import { UnsupportedFeatureError } from '../core/Errors'; + +import Form1040, { FilingStatus } from './Form1040'; +import ScheduleA from './ScheduleA'; +import TaxReturn from './TaxReturn'; +import W2 from './W2'; + +test('medical and dental expenses, limited', () => { + const p = Person.self('A'); + const tr = new TaxReturn(); + tr.addPerson(p); + tr.addForm(new W2({ + employee: p, + employer: 'E', + wages: 3000, + })); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.Single + })); + + const f = new ScheduleA({ + medicalAndDentalExpenses: 250, + }); + tr.addForm(f); + + expect(f.getValue(tr, '2')).toBe(3000); + expect(f.getValue(tr, '4')).toBe(25); + expect(f.getValue(tr, '17')).toBe(25); +}); + +test('medical and dental expenses, excluded', () => { + const p = Person.self('A'); + const tr = new TaxReturn(); + tr.addPerson(p); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.Single + })); + tr.addForm(new W2({ + employee: p, + employer: 'E', + wages: 300000, + })); + + const f = new ScheduleA({ + medicalAndDentalExpenses: 250, + }); + tr.addForm(f); + + expect(tr.getForm(Form1040).getValue(tr, '7b')).toBe(300000); + expect(f.getValue(tr, '2')).toBe(300000); + expect(f.getValue(tr, '4')).toBe(0); +}); + +test('state and local taxes, un-limited', () => { + const tr = new TaxReturn(); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint + })); + + const f = new ScheduleA({ + stateAndLocalIncomeAndSalesTaxes: 3000, + stateAndLocalRealEstateTaxes: 475, + stateAndLocalPropertyTaxes: 225, + }); + tr.addForm(f); + + expect(f.getValue(tr, '5d')).toBe(3700); + expect(f.getValue(tr, '5e')).toBe(3700); + expect(f.getValue(tr, '17')).toBe(3700); +}); + +test('state and local taxes, limited', () => { + const tr = new TaxReturn(); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint + })); + + const f = new ScheduleA({ + stateAndLocalIncomeAndSalesTaxes: 21124, + }); + tr.addForm(f); + + expect(f.getValue(tr, '5d')).toBe(21124); + expect(f.getValue(tr, '5e')).toBe(10000); + expect(f.getValue(tr, '17')).toBe(10000); +}); + +test('charitable gifts', () => { + const tr = new TaxReturn(); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint + })); + + const f = new ScheduleA({ + charitableGiftsCashOrCheck: 3000, + charitableGiftsOther: 100, + charitableCarryOver: 22, + }); + tr.addForm(f); + + expect(f.getValue(tr, '14')).toBe(3122); + expect(f.getValue(tr, '17')).toBe(3122); +}); + +test('all inputs', () => { + const p = Person.self('A'); + const tr = new TaxReturn(); + tr.addPerson(p); + tr.addForm(new W2({ + employee: p, + employer: 'E', + wages: 3000, + })); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.Single + })); + + const f = new ScheduleA({ + medicalAndDentalExpenses: 250, + stateAndLocalIncomeAndSalesTaxes: 1000, + stateAndLocalRealEstateTaxes: 1000, + stateAndLocalPropertyTaxes: 1000, + otherTaxes: 1000, + unreportedMortgageInterest: 1000, + unreportedMortagePoints: 1000, + mortgageInsurancePremiums: 1000, + investmentInterest: 1000, + charitableGiftsCashOrCheck: 1000, + charitableGiftsOther: 1000, + charitableCarryOver: 1000, + casualtyAndTheftLosses: 1000, + }); + tr.addForm(f); + + expect(f.getValue(tr, '17')).toBe(12025); +}); diff --git a/src/fed2019/ScheduleA.ts b/src/fed2019/ScheduleA.ts new file mode 100644 index 0000000..8331a62 --- /dev/null +++ b/src/fed2019/ScheduleA.ts @@ -0,0 +1,81 @@ +// Copyright 2020 Blue Static +// This program is free software licensed under the GNU General Public License, +// version 3.0. The full text of the license can be found in LICENSE.txt. +// SPDX-License-Identifier: GPL-3.0-only + +import { Form } from '../core'; +import { ComputedLine, InputLine, ReferenceLine, UnsupportedLine, sumFormLines } from '../core/Line'; +import { clampToZero } from '../core/Math'; + +import Form1040, { FilingStatus } from './Form1040'; + +export interface ScheduleAInput { + medicalAndDentalExpenses?: number; + + stateAndLocalIncomeAndSalesTaxes?: number; + stateAndLocalRealEstateTaxes?: number; + stateAndLocalPropertyTaxes?: number; + otherTaxes?: number; + + // This form does not apply the mortgage interest limitations. + unreportedMortgageInterest?: number; + unreportedMortagePoints?: number; + mortgageInsurancePremiums?: number; + investmentInterest?: number; + + charitableGiftsCashOrCheck?: number; + charitableGiftsOther?: number; + charitableCarryOver?: number; + + casualtyAndTheftLosses?: number; + + forceItemize?: boolean; +}; + +class Input extends InputLine {}; + +export default class ScheduleA extends Form { + readonly name = 'Schedule A'; + + readonly lines = { + // Medical and dental expenses + '1': new Input('medicalAndDentalExpenses', 'Medical and dental expenses', 0), + '2': new ReferenceLine(Form1040, '8b'), + '3': new ComputedLine((tr): number => this.getValue(tr, '2') * 0.075), + '4': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '3'))), + + // Taxes you paid + '5a': new Input('stateAndLocalIncomeAndSalesTaxes', 'State and local income taxes or general sales taxes', 0), + '5b': new Input('stateAndLocalRealEstateTaxes', 'State and local real estate taxes', 0), + '5c': new Input('stateAndLocalPropertyTaxes', 'State and local personal property taxes', 0), + '5d': new ComputedLine((tr): number => sumFormLines(tr, this, ['5a', '5b', '5c'])), + '5e': new ComputedLine((tr): number => { + const fs = tr.getForm(Form1040).filingStatus; + const limit = fs == FilingStatus.MarriedFilingSeparate ? 5000 : 10000; + return Math.min(this.getValue(tr, '5d'), limit); + }), + '6': new Input('otherTaxes', 'Other taxes', 0), + '7': new ComputedLine((tr): number => sumFormLines(tr, this, ['5e', '6'])), + + // Interest you paid + // TODO - Form 1098 + '8a': new UnsupportedLine('Home mortgage interest and points'), + '8b': new Input('unreportedMortgageInterest', 'Home mortgage interest not reported on Form 1098', 0), + '8c': new Input('unreportedMortagePoints', 'Points not reported on Form 1098', 0), + '8d': new Input('mortgageInsurancePremiums', 'Mortgage insurance premiums', 0), + '8e': new ComputedLine((tr): number => sumFormLines(tr, this, ['8a', '8b', '8c', '8d'])), + '9': new Input('investmentInterest', 'Investment interest', 0), + '10': new ComputedLine((tr): number => sumFormLines(tr, this, ['8e', '9'])), + + // Gifts to charity + '11': new Input('charitableGiftsCashOrCheck', 'Gifts by cash or check', 0), + '12': new Input('charitableGiftsOther', 'Other than by cash or check', 0), + '13': new Input('charitableCarryOver', 'Carryover from prior year', 0), + '14': new ComputedLine((tr): number => sumFormLines(tr, this, ['11', '12', '13'])), + + '15': new Input('casualtyAndTheftLosses', 'Casualty and theft loss(es)', 0), + + '17': new ComputedLine((tr): number => sumFormLines(tr, this, ['4', '7', '10', '14', '15'])), + '18': new Input('forceItemize', 'Itemize even if less than standard deduction', false), + }; +}; diff --git a/src/fed2019/index.ts b/src/fed2019/index.ts index 152a588..78e3b75 100644 --- a/src/fed2019/index.ts +++ b/src/fed2019/index.ts @@ -18,6 +18,7 @@ export { default as Form8995REIT } from './Form8995'; export { default as Schedule1 } from './Schedule1'; export { default as Schedule2 } from './Schedule2'; export { default as Schedule3 } from './Schedule3'; +export { default as ScheduleA } from './ScheduleA'; export { default as ScheduleD } from './ScheduleD'; export { default as TaxReturn } from './TaxReturn'; export { default as W2 } from './W2'; -- 2.22.5