From 244689af2e13c8d1bda8068984c33319eb039fbc Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Fri, 6 Aug 2021 13:04:05 -0400 Subject: [PATCH] Add support for Form 1098 and the mortgage interest deduction to Schedule A. This includes the worksheet to calculate the deduction limitation. --- package.json | 2 +- src/fed2019/Form1098.test.ts | 89 ++++++++++++++++++++++++++++ src/fed2019/Form1098.ts | 112 +++++++++++++++++++++++++++++++++++ src/fed2019/README.md | 1 + src/fed2019/ScheduleA.ts | 8 ++- src/fed2019/TaxReturn.ts | 8 +++ src/fed2019/index.ts | 1 + src/fed2020/index.ts | 1 + 8 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 src/fed2019/Form1098.test.ts create mode 100644 src/fed2019/Form1098.ts diff --git a/package.json b/package.json index 5d0897e..7e25e0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ustaxlib", - "version": "2.0.0", + "version": "2.1.0", "description": "A library for modeling individual US tax returns.", "repository": "https://github.com/rsesek/ustaxlib", "scripts": { diff --git a/src/fed2019/Form1098.test.ts b/src/fed2019/Form1098.test.ts new file mode 100644 index 0000000..b7edffc --- /dev/null +++ b/src/fed2019/Form1098.test.ts @@ -0,0 +1,89 @@ +// Copyright 2021 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 Form1040, { FilingStatus } from './Form1040'; +import { Form1098, MortgageInterestDeductionWorksheet } from './Form1098'; +import TaxReturn from './TaxReturn'; + +test('grandfathered debt', () => { + const p = Person.self('A'); + const tr = new TaxReturn(); + tr.addPerson(p); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint, + })); + tr.addForm(new Form1098({ + recipient: 'Bank', + payee: p, + mortgageInterestReceived: 20_000, + outstandingMortgagePrincipal: 2_000_000, + mortgageOriginationDate: new Date('1980-01-02'), + })); + const ws = new MortgageInterestDeductionWorksheet(); + tr.addForm(ws); + + expect(ws.getValue(tr, '1')).toBe(2_000_000); + expect(ws.getValue(tr, '2')).toBe(0); + expect(ws.getValue(tr, '5')).toBe(2_000_000); + expect(ws.getValue(tr, '7')).toBe(0); + expect(ws.getValue(tr, '11')).toBe(2_000_000); + expect(ws.deductibleMortgateInterest(tr)).toBe(20_000); +}); + +test('pre-limitation debt', () => { + const p = Person.self('A'); + const tr = new TaxReturn(); + tr.addPerson(p); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint, + })); + tr.addForm(new Form1098({ + recipient: 'Bank', + payee: p, + mortgageInterestReceived: 20_000, + outstandingMortgagePrincipal: 2_000_000, + mortgageOriginationDate: new Date('2010-01-02'), + })); + const ws = new MortgageInterestDeductionWorksheet(); + tr.addForm(ws); + + expect(ws.getValue(tr, '1')).toBe(0); + expect(ws.getValue(tr, '2')).toBe(2_000_000); + expect(ws.getValue(tr, '5')).toBe(2_000_000); + expect(ws.getValue(tr, '7')).toBe(0); + expect(ws.getValue(tr, '11')).toBe(1_000_000); + expect(ws.getValue(tr, '14')).toBe(0.5); + expect(ws.getValue(tr, '15')).toBe(10_000); + expect(ws.deductibleMortgateInterest(tr)).toBe(10_000); +}); + +test('limited debt', () => { + const p = Person.self('A'); + const tr = new TaxReturn(); + tr.addPerson(p); + tr.addForm(new Form1040({ + filingStatus: FilingStatus.MarriedFilingJoint, + })); + tr.addForm(new Form1098({ + recipient: 'Bank', + payee: p, + mortgageInterestReceived: 20_000, + outstandingMortgagePrincipal: 2_000_000, + mortgageOriginationDate: new Date('2020-01-02'), + })); + const ws = new MortgageInterestDeductionWorksheet(); + tr.addForm(ws); + + expect(ws.getValue(tr, '1')).toBe(0); + expect(ws.getValue(tr, '2')).toBe(0); + expect(ws.getValue(tr, '5')).toBe(0); + expect(ws.getValue(tr, '7')).toBe(2_000_000); + expect(ws.getValue(tr, '11')).toBe(750_000); + expect(ws.getValue(tr, '14')).toBe(0.375); + expect(ws.getValue(tr, '15')).toBe(7_500); + expect(ws.deductibleMortgateInterest(tr)).toBe(7_500); +}); diff --git a/src/fed2019/Form1098.ts b/src/fed2019/Form1098.ts new file mode 100644 index 0000000..55cf078 --- /dev/null +++ b/src/fed2019/Form1098.ts @@ -0,0 +1,112 @@ +// Copyright 2021 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 { TaxReturn, Form, Person } from '../core'; +import { InputLine, ComputedLine, FormatType, sumFormLines, sumLineOfForms } from '../core/Line'; + +import Form1040, { FilingStatus } from './Form1040'; + +export interface Form1098Input { + recipient: string; + payee: Person; + mortgageInterestReceived: number; + // This is used for computing the "average mortgage balance." Consult Pub 936 and enter + // a different value than the one on Form 1098 to use that instead for the average + // balance calculations. + outstandingMortgagePrincipal: number; + mortgageOriginationDate: Date; + refundOfOverpaidInterest?: number; + mortgageInsurancePremiums?: number; + pointsPaidOnPurchaseOfPrincipalResidence?: number; +}; + +class Input extends InputLine {}; + +export class Form1098 extends Form { + readonly name = '1098'; + + readonly supportsMultipleCopies = true; + + readonly includeJointPersonForms = true; + + person() { return this.getInput('payee'); } + + readonly lines = { + 'recipient': new Input('recipient'), + 'payee': new Input('payee'), + '1': new Input('mortgageInterestReceived'), + '2': new Input('outstandingMortgagePrincipal'), + '3': new Input('mortgageOriginationDate'), + '4': new Input('refundOfOverpaidInterest'), + '5': new Input('mortgageInsurancePremiums'), + '6': new Input('pointsPaidOnPurchaseOfPrincipalResidence'), + }; +}; + +const kGrandfatheredDate = new Date('1987-10-13'); +const kLimitationStartDate = new Date('2017-12-15'); + +// Pub. 936 Worksheet +export class MortgageInterestDeductionWorksheet extends Form { + readonly name = 'Mortgage Interest Deduction Worksheet'; + + private get1098sMatchingPredicate(tr: TaxReturn, pred: (Form1098) => boolean): Form1098[] { + return tr.findForms(Form1098).filter(pred); + } + + deductibleMortgateInterest(tr: TaxReturn): number { + const l11 = this.getValue(tr, '11'); + const l12 = this.getValue(tr, '12'); + if (l11 < l12) { + return this.getValue(tr, '15'); + } + return sumLineOfForms(tr, tr.findForms(Form1098), '1'); + } + + readonly lines = { + '1': new ComputedLine((tr): number => { + const f1098s = this.get1098sMatchingPredicate(tr, f => f.getValue(tr, '3') <= kGrandfatheredDate); + if (f1098s.length == 0) + return 0; + return sumLineOfForms(tr, f1098s, '2') / f1098s.length; + }, 'Average balance of grandfathered debt'), + '2': new ComputedLine((tr): number => { + const f1098s = this.get1098sMatchingPredicate(tr, f => { + const date = f.getValue(tr, '3'); + return date >= kGrandfatheredDate && date <= kLimitationStartDate; + }); + if (f1098s.length == 0) + return 0; + return sumLineOfForms(tr, f1098s, '2') / f1098s.length; + }, 'Average balance of pre-limitation debt'), + '3': new ComputedLine((tr): number => { + if (tr.getForm(Form1040).filingStatus == FilingStatus.MarriedFilingSeparate) + return 500_000; + return 1_000_000; + }), + '4': new ComputedLine((tr): number => Math.max(this.getValue(tr, '1'), this.getValue(tr, '3'))), + '5': new ComputedLine((tr): number => sumFormLines(tr, this, ['1', '2'])), + '6': new ComputedLine((tr): number => Math.min(this.getValue(tr, '4'), this.getValue(tr, '5'))), + '7': new ComputedLine((tr): number => { + const f1098s = this.get1098sMatchingPredicate(tr, f => f.getValue(tr, '3') > kLimitationStartDate); + if (f1098s.length == 0) + return 0; + return sumLineOfForms(tr, f1098s, '2') / f1098s.length; + }, 'Average balance of post-limitation debt'), + '8': new ComputedLine((tr): number => { + const fs = tr.getForm(Form1040).filingStatus; + return tr.constants.mortgatgeInterestDeduction.limit[fs]; + }), + '9': new ComputedLine((tr): number => Math.max(this.getValue(tr, '6'), this.getValue(tr, '8'))), + '10': new ComputedLine((tr): number => sumFormLines(tr, this, ['6', '7'])), + '11': new ComputedLine((tr): number => Math.min(this.getValue(tr, '9'), this.getValue(tr, '10')), 'Qualified loan limit'), + + '12': new ComputedLine((tr): number => sumFormLines(tr, this, ['1', '2', '7']), 'Total of all average balanaces'), + '13': new ComputedLine((tr): number => sumLineOfForms(tr, tr.findForms(Form1098), '1'), 'Total interest paid'), + '14': new ComputedLine((tr): number => this.getValue(tr, '11') / this.getValue(tr, '12'), undefined, { formatType: FormatType.Decimal }), + '15': new ComputedLine((tr): number => this.getValue(tr, '13') * this.getValue(tr, '14'), 'Deductible home mortgage interest'), + '16': new ComputedLine((tr): number => this.getValue(tr, '13') - this.getValue(tr, '15'), 'Non-deductible interest'), + }; +}; diff --git a/src/fed2019/README.md b/src/fed2019/README.md index 01c186c..93fd75f 100644 --- a/src/fed2019/README.md +++ b/src/fed2019/README.md @@ -22,6 +22,7 @@ The following forms are at least partially supported: 1040._ - **Schedule A:** Itemized deductions - **Schedule D:** Capital gains and losses +- **Form 1098:** Mortgage interest deduction - **Form 1099-B:** Proceeds from broker transactions - **Form 1099-DIV:** Dividend income - **Form 1099-INT:** Interest income diff --git a/src/fed2019/ScheduleA.ts b/src/fed2019/ScheduleA.ts index 5b238ad..f0d717e 100644 --- a/src/fed2019/ScheduleA.ts +++ b/src/fed2019/ScheduleA.ts @@ -8,6 +8,7 @@ import { Line, ComputedLine, InputLine, ReferenceLine, SymbolicLine, Unsupported import { clampToZero } from '../core/Math'; import Form1040, { FilingStatus } from './Form1040'; +import { Form1098, MortgageInterestDeductionWorksheet } from './Form1098'; export interface ScheduleAInput { medicalAndDentalExpenses?: number; @@ -58,8 +59,11 @@ export default class ScheduleA extends Form { '7': new ComputedLine((tr): number => sumFormLines(tr, this, ['5e', '6'])), // Interest you paid - // TODO - Form 1098 - '8a': new UnsupportedLine('Home mortgage interest and points'), + '8a': new ComputedLine((tr): number => { + if (tr.findForms(Form1098).length == 0) + return 0; + return tr.getForm(MortgageInterestDeductionWorksheet).deductibleMortgateInterest(tr); + }, '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), diff --git a/src/fed2019/TaxReturn.ts b/src/fed2019/TaxReturn.ts index 808281e..803f967 100644 --- a/src/fed2019/TaxReturn.ts +++ b/src/fed2019/TaxReturn.ts @@ -129,6 +129,14 @@ export const Constants = { [FilingStatus.MarriedFilingSeparate]: 97400, }, }, + + mortgatgeInterestDeduction: { + limit: { + [FilingStatus.MarriedFilingJoint]: 750_000, + [FilingStatus.Single]: 750_000, + [FilingStatus.MarriedFilingSeparate]: 375_000, + }, + }, }; export default class TaxReturn extends BaseTaxReturn { diff --git a/src/fed2019/index.ts b/src/fed2019/index.ts index 78e3b75..b72c17f 100644 --- a/src/fed2019/index.ts +++ b/src/fed2019/index.ts @@ -24,6 +24,7 @@ export { default as TaxReturn } from './TaxReturn'; export { default as W2 } from './W2'; export * from './Form1040'; +export * from './Form1098'; export * from './Form1099B'; export * from './Form1099R'; export * from './Form1116'; diff --git a/src/fed2020/index.ts b/src/fed2020/index.ts index de94206..7e5fb29 100644 --- a/src/fed2020/index.ts +++ b/src/fed2020/index.ts @@ -28,6 +28,7 @@ export { default as ScheduleD } from '../fed2019/ScheduleD'; export { default as W2 } from '../fed2019/W2'; export { FilingStatus, Form1040Input, computeTax } from '../fed2019/Form1040'; +export * from '../fed2019/Form1098'; export * from '../fed2019/Form1099B'; export * from '../fed2019/Form1099R'; export * from '../fed2019/Form1116'; -- 2.22.5