Add Schedule A for itemized deductions.
authorRobert Sesek <rsesek@bluestatic.org>
Mon, 13 Jul 2020 20:28:41 +0000 (16:28 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Mon, 13 Jul 2020 20:28:41 +0000 (16:28 -0400)
src/fed2019/Form1040.test.ts
src/fed2019/Form1040.ts
src/fed2019/README.md
src/fed2019/ScheduleA.test.ts [new file with mode: 0644]
src/fed2019/ScheduleA.ts [new file with mode: 0644]
src/fed2019/index.ts

index 109b74f3c207248a558fa0f2cb6ae32096483347..152dd6cf9196b3dba3d6310c7df02f2c573c7714 100644 (file)
@@ -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);
+});
index 4a8b2d0cb9a239c95755e77be3dfdf2aff7a4085..579e8eb7331347bf2ba1b02df62d2d4c180c8118 100644 (file)
@@ -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<Form1040['lines'], Form1040Input> {
       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'),
 
index b0a46ffb6daa4c3c2ee055ff2a76093ccfeda4d2..01c186c04f094fcccc677ea7ed7d01915fbd1fdb 100644 (file)
@@ -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 (file)
index 0000000..53ad2d5
--- /dev/null
@@ -0,0 +1,142 @@
+// Copyright 2020 Blue Static <https://www.bluestatic.org>
+// 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 (file)
index 0000000..8331a62
--- /dev/null
@@ -0,0 +1,81 @@
+// Copyright 2020 Blue Static <https://www.bluestatic.org>
+// 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<ScheduleAInput> {};
+
+export default class ScheduleA extends Form<ScheduleA['lines'], ScheduleAInput> {
+  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),
+  };
+};
index 152a588c7d9acb15200cad0dff37857cbf71bde4..78e3b757c8bdc31c96c5965f13efa18e2e14618f 100644 (file)
@@ -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';