diff --git a/changelog.d/vt-2017-taxable-income.fixed.md b/changelog.d/vt-2017-taxable-income.fixed.md new file mode 100644 index 00000000000..773a65e7d9b --- /dev/null +++ b/changelog.d/vt-2017-taxable-income.fixed.md @@ -0,0 +1 @@ +Start Vermont's 2017 taxable income from federal taxable income, with the former 32 V.S.A. § 5811(21) itemized deduction addback, additions and subtractions, and add the 2017 federal standard deduction amounts. diff --git a/policyengine_us/parameters/gov/irs/deductions/standard/aged_or_blind/amount.yaml b/policyengine_us/parameters/gov/irs/deductions/standard/aged_or_blind/amount.yaml index d78354841f2..c73dc7f2759 100644 --- a/policyengine_us/parameters/gov/irs/deductions/standard/aged_or_blind/amount.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/standard/aged_or_blind/amount.yaml @@ -1,5 +1,8 @@ description: Additional standard deduction for blind and aged SINGLE: + 2015-01-01: 1_550 + 2016-01-01: 1_550 + 2017-01-01: 1_550 2018-01-01: 1_600 2019-01-01: 1_650 2021-01-01: 1_700 @@ -9,6 +12,9 @@ SINGLE: 2025-01-01: 2_000 2026-01-01: 2_050 JOINT: + 2015-01-01: 1_250 + 2016-01-01: 1_250 + 2017-01-01: 1_250 2018-01-01: 1_300 2019-01-01: 1_300 2021-01-01: 1_350 @@ -18,6 +24,9 @@ JOINT: 2025-01-01: 1_600 2026-01-01: 1_650 SEPARATE: + 2015-01-01: 1_250 + 2016-01-01: 1_250 + 2017-01-01: 1_250 2018-01-01: 1_300 2019-01-01: 1_300 2021-01-01: 1_350 @@ -27,6 +36,9 @@ SEPARATE: 2025-01-01: 1_600 2026-01-01: 1_650 HEAD_OF_HOUSEHOLD: + 2015-01-01: 1_550 + 2016-01-01: 1_550 + 2017-01-01: 1_550 2018-01-01: 1_600 2019-01-01: 1_650 2021-01-01: 1_700 @@ -36,6 +48,9 @@ HEAD_OF_HOUSEHOLD: 2025-01-01: 2_000 2026-01-01: 2_050 SURVIVING_SPOUSE: + 2015-01-01: 1_250 + 2016-01-01: 1_250 + 2017-01-01: 1_250 2018-01-01: 1_300 2019-01-01: 1_300 2021-01-01: 1_350 @@ -52,6 +67,12 @@ metadata: period: year propagate_metadata_to_children: true reference: + - title: 2017 IRS data release (Rev. Proc. 2016-55) + href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=15 + - title: 2016 IRS data release (Rev. Proc. 2015-53) + href: https://www.irs.gov/pub/irs-drop/rp-15-53.pdf#page=14 + - title: 2015 IRS data release (Rev. Proc. 2014-61) + href: https://www.irs.gov/pub/irs-drop/rp-14-61.pdf#page=13 - title: 2026 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=18 - title: 2025 IRS data release diff --git a/policyengine_us/parameters/gov/irs/deductions/standard/amount.yaml b/policyengine_us/parameters/gov/irs/deductions/standard/amount.yaml index cd56afc4fd0..75562208a5a 100644 --- a/policyengine_us/parameters/gov/irs/deductions/standard/amount.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/standard/amount.yaml @@ -1,6 +1,9 @@ description: Federal deduction from AGI if not itemizing. SINGLE: + 2015-01-01: 6_300 + 2016-01-01: 6_300 + 2017-01-01: 6_350 2018-01-01: 12_000 2019-01-01: 12_200 2020-01-01: 12_400 @@ -11,6 +14,9 @@ SINGLE: 2025-01-01: 15_750 2026-01-01: 16_100 JOINT: + 2015-01-01: 12_600 + 2016-01-01: 12_600 + 2017-01-01: 12_700 2018-01-01: 24_000 2019-01-01: 24_400 2020-01-01: 24_800 @@ -21,6 +27,9 @@ JOINT: 2025-01-01: 31_500 2026-01-01: 32_200 SEPARATE: + 2015-01-01: 6_300 + 2016-01-01: 6_300 + 2017-01-01: 6_350 2018-01-01: 12_000 2019-01-01: 12_200 2020-01-01: 12_400 @@ -31,6 +40,9 @@ SEPARATE: 2025-01-01: 15_750 2026-01-01: 16_100 HEAD_OF_HOUSEHOLD: + 2015-01-01: 9_250 + 2016-01-01: 9_300 + 2017-01-01: 9_350 2018-01-01: 18_000 2019-01-01: 18_350 2020-01-01: 18_650 @@ -41,6 +53,9 @@ HEAD_OF_HOUSEHOLD: 2025-01-01: 23_625 2026-01-01: 24_150 SURVIVING_SPOUSE: + 2015-01-01: 12_600 + 2016-01-01: 12_600 + 2017-01-01: 12_700 2018-01-01: 24_000 2019-01-01: 24_400 2020-01-01: 24_800 @@ -76,6 +91,12 @@ metadata: href: https://www.irs.gov/pub/irs-drop/rp-19-44.pdf - title: 2019 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-18-57.pdf + - title: 2017 IRS data release (Rev. Proc. 2016-55) + href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=15 + - title: 2016 IRS data release (Rev. Proc. 2015-53) + href: https://www.irs.gov/pub/irs-drop/rp-15-53.pdf#page=13 + - title: 2015 IRS data release (Rev. Proc. 2014-61) + href: https://www.irs.gov/pub/irs-drop/rp-14-61.pdf#page=13 - title: CBO Tax Parameters and Effective Marginal Tax Rates | Feb 2024 # 1. Tax Parameters. href: https://www.cbo.gov/system/files/2024-02/53724-2024-02-Tax-Parameters.xlsx diff --git a/policyengine_us/parameters/gov/states/vt/tax/income/deductions/itemized_addback/other_multiple.yaml b/policyengine_us/parameters/gov/states/vt/tax/income/deductions/itemized_addback/other_multiple.yaml new file mode 100644 index 00000000000..b3c982cb833 --- /dev/null +++ b/policyengine_us/parameters/gov/states/vt/tax/income/deductions/itemized_addback/other_multiple.yaml @@ -0,0 +1,10 @@ +description: Vermont adds back itemized deductions, other than income taxes, medical expenses and charitable contributions, in excess of this multiple of the federal standard deduction, under former 32 V.S.A. § 5811(21)(A)(iv). +values: + 2017-01-01: 2.5 +metadata: + unit: /1 + period: year + label: Vermont 2017 other itemized deduction addback standard deduction multiple + reference: + - title: 2017 Vt. Acts No. 73, § 13a (former 32 V.S.A. § 5811(21)(A)(iii)-(iv)) #page 21-22 + href: https://legislature.vermont.gov/Documents/2018/Docs/ACTS/ACT073/ACT073%20As%20Enacted.pdf#page=21 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/additional_standard_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/additional_standard_deduction.yaml new file mode 100644 index 00000000000..a5dd34ab9e8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/additional_standard_deduction.yaml @@ -0,0 +1,73 @@ +- name: 2017 aged single filer adds $1,550 + period: 2017 + input: + filing_status: SINGLE + aged_blind_count: 1 + output: + additional_standard_deduction: 1_550 + +- name: 2017 aged and blind head of household adds 2 x $1,550 + period: 2017 + input: + filing_status: HEAD_OF_HOUSEHOLD + aged_blind_count: 2 + output: + additional_standard_deduction: 3_100 + +- name: 2017 joint filers with one aged spouse add $1,250 + period: 2017 + input: + filing_status: JOINT + aged_blind_count: 1 + output: + additional_standard_deduction: 1_250 + +- name: 2017 separate filer aged adds $1,250 + period: 2017 + input: + filing_status: SEPARATE + aged_blind_count: 1 + output: + additional_standard_deduction: 1_250 + +- name: 2017 aged surviving spouse adds $1,250 + period: 2017 + input: + filing_status: SURVIVING_SPOUSE + aged_blind_count: 1 + output: + additional_standard_deduction: 1_250 + +- name: 2015 single filer aged 65 adds $1,550 + period: 2015 + input: + people: + head: + age: 65 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + output: + additional_standard_deduction: 1_550 + +- name: 2016 single filer aged 65 adds $1,550 + period: 2016 + input: + people: + head: + age: 65 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + output: + additional_standard_deduction: 1_550 + +- name: 2018 aged single filer adds $1,600, unchanged + period: 2018 + input: + filing_status: SINGLE + aged_blind_count: 1 + output: + additional_standard_deduction: 1_600 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml index a22f9f3c55d..5e63d8f9df0 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml @@ -574,3 +574,73 @@ irs_gross_income: 15_000 basic_standard_deduction: 15_450 tax_unit_is_required_to_file: false + +- name: 2017 single filer standard deduction is $6,350 + period: 2017 + input: + filing_status: SINGLE + output: + basic_standard_deduction: 6_350 + +- name: 2017 separate filer standard deduction is $6,350 + period: 2017 + input: + filing_status: SEPARATE + output: + basic_standard_deduction: 6_350 + +- name: 2017 head of household standard deduction is $9,350 + period: 2017 + input: + filing_status: HEAD_OF_HOUSEHOLD + output: + basic_standard_deduction: 9_350 + +- name: 2017 joint filer standard deduction is $12,700 + period: 2017 + input: + filing_status: JOINT + output: + basic_standard_deduction: 12_700 + +- name: 2017 surviving spouse standard deduction is $12,700 + period: 2017 + input: + filing_status: SURVIVING_SPOUSE + output: + basic_standard_deduction: 12_700 + +- name: 2015 head of household standard deduction is $9,250 + period: 2015 + input: + filing_status: HEAD_OF_HOUSEHOLD + output: + basic_standard_deduction: 9_250 + +- name: 2015 joint filers get a $12,600 standard deduction + period: 2015 + input: + filing_status: JOINT + output: + basic_standard_deduction: 12_600 + +- name: 2015 single filers get a $6,300 standard deduction + period: 2015 + input: + filing_status: SINGLE + output: + basic_standard_deduction: 6_300 + +- name: 2016 head of household standard deduction is $9,300 + period: 2016 + input: + filing_status: HEAD_OF_HOUSEHOLD + output: + basic_standard_deduction: 9_300 + +- name: 2016 joint filer standard deduction is $12,600 + period: 2016 + input: + filing_status: JOINT + output: + basic_standard_deduction: 12_600 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_itemized_deduction_addback.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_itemized_deduction_addback.yaml new file mode 100644 index 00000000000..671bdc0aa63 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_itemized_deduction_addback.yaml @@ -0,0 +1,263 @@ +- name: A 2017 nonitemizer adds nothing back. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: false + itemized_taxable_income_deductions: 30_000 + standard_deduction: 6_350 + salt_deduction: 2_000 + state_withheld_income_tax: 2_000 + output: + vt_2017_itemized_deduction_addback: 0 + +- name: 2017 adds back income taxes and other deductions above 2.5 times the standard deduction. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 30_000 + standard_deduction: 6_350 + salt_deduction: 2_000 + state_withheld_income_tax: 2_000 + medical_expense_deduction: 0 + charitable_deduction: 0 + output: + # min(2,000, 30,000 - 6,350) + max(30,000 - 2,000 - 2.5 x 6,350, 0) + # = 2,000 + 12,125 + vt_2017_itemized_deduction_addback: 14_125 + +- name: 2017 income tax addback is limited to itemized deductions above the standard deduction. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 7_000 + standard_deduction: 6_350 + salt_deduction: 3_000 + state_withheld_income_tax: 3_000 + medical_expense_deduction: 0 + charitable_deduction: 0 + output: + # min(3,000, 650) + max(7,000 - 3,000 - 15,875, 0) + vt_2017_itemized_deduction_addback: 650 + +- name: 2017 sales tax deducted instead of income tax is not added back. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 25_000 + standard_deduction: 6_350 + salt_deduction: 3_000 + state_withheld_income_tax: 1_000 + state_sales_tax: 3_000 + medical_expense_deduction: 2_000 + charitable_deduction: 3_000 + output: + # T = 0; max(25,000 - 0 - 2,000 - 3,000 - 15,875, 0) = 4,125 + vt_2017_itemized_deduction_addback: 4_125 + +- name: 2017 medical and charitable deductions are not in the other-deduction pool. + period: 2017 + input: + state_code: VT + filing_status: JOINT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 40_000 + standard_deduction: 12_700 + salt_deduction: 5_000 + state_withheld_income_tax: 5_000 + medical_expense_deduction: 10_000 + charitable_deduction: 5_000 + output: + # min(5,000, 27,300) + max(40,000 - 5,000 - 10,000 - 5,000 - 31,750, 0) + vt_2017_itemized_deduction_addback: 5_000 + +- name: The addback only applies in 2017. + period: 2018 + input: + state_code: VT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 30_000 + standard_deduction: 12_000 + salt_deduction: 2_000 + state_withheld_income_tax: 2_000 + output: + vt_2017_itemized_deduction_addback: 0 + +- name: 2017 counts wagering losses among the other itemized deductions. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 12_850 + wagering_losses_deduction: 30_000 + standard_deduction: 6_350 + salt_deduction: 4_850 + state_withheld_income_tax: 4_850 + medical_expense_deduction: 0 + charitable_deduction: 0 + output: + # IN-155 line 1 takes Schedule A line 29, so I = 12,850 + 30,000 = 42,850. + # min(4,850, 42,850 - 6,350) + max(42,850 - 4,850 - 2.5 x 6,350, 0) + # = 4,850 + 22,125 + vt_2017_itemized_deduction_addback: 26_975 + +- name: 2017 scales deducted income taxes by the share section 68 leaves. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: true + total_itemized_taxable_income_deductions: 44_000 + itemized_taxable_income_deductions_reduction: 4_000 + itemized_taxable_income_deductions: 40_000 + standard_deduction: 6_350 + salt_deduction: 20_000 + state_withheld_income_tax: 20_000 + medical_expense_deduction: 4_000 + charitable_deduction: 10_000 + output: + # Limited base 44,000 - 4,000 medical = 40,000; r = 1 - 4,000 / 40,000 = 0.9. + # T = 18,000, C = 9,000. + # min(18,000, 40,000 - 6,350) + max(40,000 - 18,000 - 4,000 - 9,000 - 15,875, 0) + # = 18,000 + 0 (unscaled: 20,000) + vt_2017_itemized_deduction_addback: 18_000 + +- name: 2017 scales charitable deductions by the share section 68 leaves. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: true + total_itemized_taxable_income_deductions: 74_000 + itemized_taxable_income_deductions_reduction: 7_000 + itemized_taxable_income_deductions: 67_000 + standard_deduction: 6_350 + salt_deduction: 20_000 + state_withheld_income_tax: 20_000 + medical_expense_deduction: 4_000 + charitable_deduction: 10_000 + output: + # Limited base 74,000 - 4,000 = 70,000; r = 1 - 7,000 / 70,000 = 0.9. + # T = 18,000, C = 9,000. + # min(18,000, 67,000 - 6,350) + max(67,000 - 18,000 - 4,000 - 9,000 - 15,875, 0) + # = 18,000 + 20,125 (unscaled: 37,125) + vt_2017_itemized_deduction_addback: 38_125 + +- name: 2017 leaves income taxes whole when no deductions are subject to section 68. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: true + total_itemized_taxable_income_deductions: 8_000 + itemized_taxable_income_deductions_reduction: 0 + itemized_taxable_income_deductions: 8_000 + standard_deduction: 6_350 + salt_deduction: 2_000 + state_withheld_income_tax: 2_000 + medical_expense_deduction: 8_000 + charitable_deduction: 0 + output: + # Limited base max(8,000 - 8,000, 0) = 0, so r = 1. + # min(2,000, 8,000 - 6,350) + max(8,000 - 2,000 - 8,000 - 15,875, 0) = 1,650 + vt_2017_itemized_deduction_addback: 1_650 + +- name: 2017 addback through the section 68 limitation, end to end. + period: 2017 + absolute_error_margin: 0.01 + input: + state_code: VT + tax_unit_itemizes: true + adjusted_gross_income: 500_000 + salt_deduction: 20_000 + state_withheld_income_tax: 20_000 + charitable_deduction: 10_000 + medical_expense_deduction: 4_000 + interest_deduction: 40_000 + standard_deduction: 6_350 + output: + # Total 20,000 + 10,000 + 4,000 + 40,000 = 74,000; limited base 70,000. + # Reduction min((500,000 - 261,500) x 3%, 80% x 70,000) = 7,155. + # I = 66,845; r = 62,845 / 70,000. + # T = 17,955.71, C = 8,977.86. + # min(17,955.71, 60,495) + max(66,845 - 17,955.71 - 4,000 - 8,977.86 - 15,875, 0) + # = 17,955.71 + 20,036.43 + itemized_taxable_income_deductions_reduction: 7_155 + itemized_taxable_income_deductions: 66_845 + vt_2017_itemized_deduction_addback: 37_992.14 + +- name: 2017 separate filer whose spouse doesn't itemize measures the addback against the standard deduction. + period: 2017 + absolute_error_margin: 0.01 + input: + state_code: VT + filing_status: SEPARATE + tax_unit_itemizes: true + separate_filer_itemizes: false + employment_income: 100_000 + real_estate_taxes: 8_000 + state_withheld_income_tax: 4_850 + output: + # Schedule A line 29: 4,850 state income tax + 8,000 real estate tax + # = 12,850, with no section 68 cut. + # Line 1 12,850; line 2 6,350; line 3 6,500. + # Line 4 4,850; lines 5 and 6 0; line 7 4,850. + # Line 8 8,000; line 9 2.5 x 6,350 = 15,875; line 10 0. + # Line 11 min(6,500, 4,850) + 0 = 4,850. + standard_deduction: 6_350 + vt_2017_itemized_deduction_addback: 4_850 + # 100,000 - 4,050 exemption - 12,850 itemized + 4,850 addback + vt_taxable_income: 87_950 + +- name: 2017 separate filer whose spouse itemizes has no standard deduction on line 2. + period: 2017 + absolute_error_margin: 0.01 + input: + state_code: VT + filing_status: SEPARATE + tax_unit_itemizes: true + separate_filer_itemizes: true + employment_income: 100_000 + real_estate_taxes: 8_000 + state_withheld_income_tax: 4_850 + output: + # The filer's spouse itemizes, so the filer's standard deduction falls + # to 0 (26 U.S.C. 63(c)(6)(A)). Schedule A line 29: 12,850, as in the + # case above. + # Line 1 12,850; line 2 0; line 3 12,850. + # Line 4 4,850; lines 5 and 6 0; line 7 4,850. + # Line 8 8,000; line 9 0; line 10 8,000. + # Line 11 min(12,850, 4,850) + 8,000 = 12,850. + standard_deduction: 0 + vt_2017_itemized_deduction_addback: 12_850 + # 100,000 - 4,050 exemption - 12,850 itemized + 12,850 addback + vt_taxable_income: 95_950 + +- name: 2017 single filer aged 65 adds the aged amount to line 2. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + head: + age: 65 + employment_income: 100_000 + real_estate_taxes: 20_000 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: true + state_withheld_income_tax: 4_850 + households: + household: + members: [head] + state_code: VT + output: + # Line 1 4,850 + 20,000 = 24,850; line 2 6,350 + 1,550 aged = 7,900; + # line 3 16,950. Line 4 4,850; line 8 20,000; line 9 2.5 x 7,900 = + # 19,750; line 10 250. Line 11 min(16,950, 4,850) + 250 = 5,100. + # Without the aged amount, line 9 would come to 15,875 and the + # addback to 8,975. + standard_deduction: 7_900 + vt_2017_itemized_deduction_addback: 5_100 + # 100,000 - 4,050 - 24,850 + 5,100 + vt_taxable_income: 76_200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_other_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_other_additions.yaml new file mode 100644 index 00000000000..bddb1aa2f3e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_other_additions.yaml @@ -0,0 +1,8 @@ +- name: 2017 other additions total tax-exempt interest and other additions. + period: 2017 + input: + state_code: VT + tax_exempt_interest_income: 1_500 + vt_other_additions: 700 + output: + vt_2017_other_additions: 2_200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_subtractions.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_subtractions.yaml new file mode 100644 index 00000000000..d42e347da8f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/deductions/vt_2017_subtractions.yaml @@ -0,0 +1,17 @@ +- name: 2017 subtractions total U.S. obligation interest, taxable state refunds and other subtractions. + period: 2017 + input: + state_code: VT + us_govt_interest: 1_000 + salt_refund_income: 300 + vt_2017_other_subtractions: 2_000 + output: + vt_2017_subtractions: 3_300 + +- name: No subtractions outside Vermont. + period: 2017 + input: + state_code: NH + us_govt_interest: 1_000 + output: + vt_2017_subtractions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_taxable_income.yaml index 548e36c989c..2a82cd9db15 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_taxable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_taxable_income.yaml @@ -34,3 +34,129 @@ state_code: VT output: vt_taxable_income: 0 + +- name: 2017 uses federal deductions and historical adjustments. + period: 2017 + input: + state_code: VT + adjusted_gross_income: 50_000 + exemptions: 4_050 + taxable_income_deductions: 20_000 + vt_2017_itemized_deduction_addback: 2_000 + vt_2017_other_additions: 1_000 + vt_2017_subtractions: 500 + # The 2018+ Vermont-base inputs below do not enter 2017 taxable income. + vt_agi: 49_500 + vt_standard_deduction: 6_350 + vt_personal_exemptions: 4_050 + output: + # 50,000 - 4,050 - 20,000 + 2,000 + 1,000 - 500 + vt_taxable_income: 28_450 + +- name: 2017 restores excess other itemized deductions. + period: 2017 + input: + state_code: VT + adjusted_gross_income: 70_000 + exemptions: 4_050 + taxable_income_deductions: 30_000 + vt_2017_itemized_deduction_addback: 14_125 + vt_2017_other_additions: 0 + vt_2017_subtractions: 0 + output: + # 70,000 - 4,050 - 30,000 + 14,125 + vt_taxable_income: 50_075 + +- name: 2017 keeps a negative federal base before Vermont additions. + period: 2017 + input: + state_code: VT + adjusted_gross_income: 5_000 + exemptions: 4_050 + taxable_income_deductions: 6_350 + taxable_income: 0 + vt_2017_itemized_deduction_addback: 0 + vt_2017_other_additions: 1_000 + vt_2017_subtractions: 0 + output: + # 5,000 - 4,050 - 6,350 + 1,000 = -4,400, floored at zero. Starting + # from floored federal taxable income would give $1,000. + vt_taxable_income: 0 + +- name: 2017 subtractions cannot push taxable income below zero. + period: 2017 + input: + state_code: VT + adjusted_gross_income: 20_000 + exemptions: 4_050 + taxable_income_deductions: 6_350 + vt_2017_itemized_deduction_addback: 0 + vt_2017_other_additions: 0 + vt_2017_subtractions: 15_000 + output: + vt_taxable_income: 0 + +- name: 2018 still starts from Vermont AGI. + period: 2018 + input: + state_code: VT + adjusted_gross_income: 50_000 + exemptions: 0 + taxable_income_deductions: 20_000 + vt_agi: 40_000 + vt_standard_deduction: 6_000 + vt_personal_exemptions: 4_000 + output: + vt_taxable_income: 30_000 + +- name: 2016 keeps the Vermont-base formula. + period: 2016 + input: + state_code: VT + adjusted_gross_income: 50_000 + exemptions: 0 + taxable_income_deductions: 20_000 + vt_agi: 45_000 + vt_standard_deduction: 6_000 + vt_personal_exemptions: 4_000 + output: + # 45,000 - 6,000 - 4,000. The 2017 federal base would give + # 50,000 - 0 - 20,000 = 30,000, and no formula would give 0. + vt_taxable_income: 35_000 + +- name: 2017 starts from the federal base, not Vermont AGI less Vermont deductions. + period: 2017 + input: + state_code: VT + tax_unit_itemizes: false + adjusted_gross_income: 50_000 + exemptions: 4_050 + taxable_income_deductions: 20_000 + vt_agi: 49_500 + vt_standard_deduction: 6_350 + vt_personal_exemptions: 4_050 + output: + # 50,000 - 4,050 - 20,000, with no addback, additions or subtractions. + # The Vermont-base formula would give 49,500 - 6,350 - 4,050 = 39,100. + vt_taxable_income: 25_950 + +- name: 2017 single wage earner, end to end from the federal base. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: VT + output: + # 60,000 - 4,050 exemption - 6,350 standard deduction. + standard_deduction: 6_350 + exemptions: 4_050 + vt_taxable_income: 49_600 diff --git a/policyengine_us/tests/test_vt_2017_taxable_income_properties.py b/policyengine_us/tests/test_vt_2017_taxable_income_properties.py new file mode 100644 index 00000000000..1cceb17a680 --- /dev/null +++ b/policyengine_us/tests/test_vt_2017_taxable_income_properties.py @@ -0,0 +1,243 @@ +"""Property tests: Vermont's 2017 taxable income under former 32 V.S.A. +§ 5811(21). + +For 2017, Vermont taxable income is federal taxable income, before its zero +floor, plus the itemized deduction addback and other additions, less the +subtractions, floored at zero. Schedule IN-155 computes the addback. For +allowed federal itemized deductions I (Schedule A line 29, after section 68 +and including wagering losses), deducted income taxes T, medical M, +charitable C and federal standard deduction S, it is min(rT, max(I - S, 0)) ++ max(I - rT - M - rC - 2.5 S, 0) for an itemizer and zero otherwise. r is +the Department's worksheet ratio: the share of the deductions section 68 +limits that remains after the limitation (1 when none are limited). + +Properties checked, each in one batched simulation per example: + +1. max(I - SALT - M - C - 2.5 S, 0) <= addback <= max(I - S, 0) + + max(I - 2.5 S, 0) for an itemizer, and the addback is zero for a + nonitemizer. +2. The addback never falls when I rises with T, M, C, S and r held fixed. +3. The addback matches in155_line_11, a reference that follows Schedule + IN-155 lines 1 to 11 and the worksheet's cells L8 to L10. +4. vt_taxable_income >= 0, and does not depend on the 2018+ Vermont base + (vt_agi, vt_standard_deduction, vt_personal_exemptions). +5. Raising the other additions by d raises vt_taxable_income by between 0 + and d, and by exactly d when the original value is positive. + +Properties 4 and 5 fail if 2017 uses the 2018+ formula, which ignores the +additions and reads the Vermont base. The model computes in single +precision, so comparisons allow one cent or eight float32 spacings. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2017 +MAX_AMOUNT = 100_000 +SETTINGS = settings( + max_examples=15, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) + +amount = st.integers(min_value=0, max_value=MAX_AMOUNT) + + +def tolerance(*amounts): + scale = np.float32(max(np.max(np.abs(a)) for a in amounts)) + return max(0.01, 8 * float(np.spacing(scale))) + + +def calculate(units, variable): + """One Vermont tax unit per household, each with its own inputs.""" + people, tax_units, households = {}, {}, {} + for i, unit in enumerate(units): + people[f"p{i}"] = {"age": {YEAR: 40}} + tax_units[f"t{i}"] = { + "members": [f"p{i}"], + **{name: {YEAR: value} for name, value in unit.items()}, + } + households[f"h{i}"] = { + "members": [f"p{i}"], + "state_code": {YEAR: "VT"}, + } + situation = { + "people": people, + "tax_units": tax_units, + "households": households, + } + return Simulation(situation=situation).calculate(variable, YEAR) + + +def complete_addback_unit(draw): + """Build total itemized deductions from their parts, and draw the section + 68 reduction within its ceiling: 80% of the deductions it limits.""" + unit = dict(draw) + unit["total_itemized_taxable_income_deductions"] = total = ( + unit["salt_deduction"] + + unit["medical_expense_deduction"] + + unit["charitable_deduction"] + + unit.pop("other_itemized") + ) + ceiling = 0.8 * max(total - unit["medical_expense_deduction"], 0) + reduction = round(unit.pop("reduction_share") * ceiling) + unit["itemized_taxable_income_deductions_reduction"] = reduction + unit["itemized_taxable_income_deductions"] = total - reduction + return unit + + +addback_unit = st.fixed_dictionaries( + { + "tax_unit_itemizes": st.booleans(), + "other_itemized": amount, + "reduction_share": st.sampled_from([0, 0.25, 0.5, 1]), + "wagering_losses_deduction": st.one_of(st.just(0), amount), + "standard_deduction": st.integers(min_value=0, max_value=20_000), + "salt_deduction": amount, + "state_withheld_income_tax": amount, + "local_income_tax": st.just(0), + "state_sales_tax": amount, + "local_sales_tax": st.just(0), + "medical_expense_deduction": amount, + "charitable_deduction": amount, + } +).map(complete_addback_unit) + + +def itemized_total(unit): + return ( + unit["itemized_taxable_income_deductions"] + unit["wagering_losses_deduction"] + ) + + +def in155_line_11(unit): + """The 2017 Schedule IN-155 addback, line by line, with the Department's + worksheet for limited itemized deductions on lines 4 and 6.""" + if not unit["tax_unit_itemizes"]: + return 0.0 + # Worksheet: L8 total deductions, L9 deductions not limited at the + # federal level (medical and wagering losses here), L10 total deductions + # after limitations. + l8 = ( + unit["total_itemized_taxable_income_deductions"] + + unit["wagering_losses_deduction"] + ) + l9 = unit["medical_expense_deduction"] + unit["wagering_losses_deduction"] + l10 = itemized_total(unit) + ratio = (l10 - l9) / (l8 - l9) if l8 > l9 else 1.0 + # Schedule A line 5a holds income taxes only when the filer elects them + # over sales taxes. + income_taxes = unit["state_withheld_income_tax"] + unit["local_income_tax"] + sales_taxes = unit["state_sales_tax"] + unit["local_sales_tax"] + schedule_a_5a = ( + min(income_taxes, unit["salt_deduction"]) if income_taxes >= sales_taxes else 0 + ) + line_1 = itemized_total(unit) # Schedule A line 29 + line_2 = unit["standard_deduction"] + # A filer whose itemized deductions are below the standard deduction has + # nothing to add back on line 3. + line_3 = max(line_1 - line_2, 0) + line_4 = schedule_a_5a * ratio + line_5 = unit["medical_expense_deduction"] + line_6 = unit["charitable_deduction"] * ratio + line_7 = line_4 + line_5 + line_6 + line_8 = line_1 - line_7 + line_9 = line_2 * 2.5 + line_10 = max(line_8 - line_9, 0) + return min(line_3, line_4) + line_10 + + +@SETTINGS +@given(units=st.lists(addback_unit, min_size=1, max_size=20), raise_by=amount) +def test_addback_bounds_and_monotonicity(units, raise_by): + addback = calculate(units, "vt_2017_itemized_deduction_addback") + itemized = np.array([itemized_total(u) for u in units]) + standard = np.array([u["standard_deduction"] for u in units]) + itemizes = np.array([u["tax_unit_itemizes"] for u in units]) + upper = np.maximum(itemized - standard, 0) + np.maximum( + itemized - 2.5 * standard, 0 + ) + eps = tolerance(addback, upper) + assert np.all(addback >= -eps) + assert np.all(addback <= upper + eps) + assert np.all(np.abs(addback[~itemizes]) <= eps) + # Scaled income taxes are at most the SALT deduction, so the other + # deductions are at least I - SALT - M - C. + salt = np.array([u["salt_deduction"] for u in units]) + medical = np.array([u["medical_expense_deduction"] for u in units]) + charitable = np.array([u["charitable_deduction"] for u in units]) + lower = np.maximum(itemized - salt - medical - charitable - 2.5 * standard, 0) + assert np.all(addback[itemizes] >= lower[itemizes] - eps) + + raised = [ + { + **u, + "itemized_taxable_income_deductions": u[ + "itemized_taxable_income_deductions" + ] + + raise_by, + } + for u in units + ] + raised_addback = calculate(raised, "vt_2017_itemized_deduction_addback") + assert np.all(raised_addback >= addback - tolerance(addback, raised_addback)) + + +@SETTINGS +@given(units=st.lists(addback_unit, min_size=1, max_size=20)) +def test_addback_matches_schedule_in155(units): + addback = calculate(units, "vt_2017_itemized_deduction_addback") + expected = np.array([in155_line_11(u) for u in units]) + assert np.all(np.abs(addback - expected) <= tolerance(addback, expected)) + + +taxable_income_unit = st.fixed_dictionaries( + { + "adjusted_gross_income": st.integers(min_value=-20_000, max_value=MAX_AMOUNT), + "exemptions": st.integers(min_value=0, max_value=20_000), + "taxable_income_deductions": st.integers(min_value=0, max_value=50_000), + "vt_2017_itemized_deduction_addback": st.integers( + min_value=0, max_value=20_000 + ), + "vt_2017_other_additions": st.integers(min_value=0, max_value=20_000), + "vt_2017_subtractions": st.integers(min_value=0, max_value=20_000), + "vt_agi": amount, + "vt_standard_deduction": st.integers(min_value=0, max_value=20_000), + "vt_personal_exemptions": st.integers(min_value=0, max_value=20_000), + } +) + + +@SETTINGS +@given( + units=st.lists(taxable_income_unit, min_size=1, max_size=20), + vermont_base_shift=st.integers(min_value=1, max_value=50_000), + raise_by=st.integers(min_value=1, max_value=20_000), +) +def test_taxable_income_floor_base_and_additions(units, vermont_base_shift, raise_by): + taxable = calculate(units, "vt_taxable_income") + assert np.all(taxable >= 0) + + shifted = [{**u, "vt_agi": u["vt_agi"] + vermont_base_shift} for u in units] + shifted_taxable = calculate(shifted, "vt_taxable_income") + assert np.all( + np.abs(shifted_taxable - taxable) <= tolerance(taxable, shifted_taxable) + ) + + raised = [ + { + **u, + "vt_2017_other_additions": u["vt_2017_other_additions"] + raise_by, + } + for u in units + ] + raised_taxable = calculate(raised, "vt_taxable_income") + change = raised_taxable - taxable + eps = tolerance(taxable, raised_taxable) + assert np.all(change >= -eps) + assert np.all(change <= raise_by + eps) + positive = taxable > eps + assert np.all(np.abs(change[positive] - raise_by) <= eps) diff --git a/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_itemized_deduction_addback.py b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_itemized_deduction_addback.py new file mode 100644 index 00000000000..7f4cf5fffbd --- /dev/null +++ b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_itemized_deduction_addback.py @@ -0,0 +1,75 @@ +from policyengine_us.model_api import * + + +class vt_2017_itemized_deduction_addback(Variable): + value_type = float + entity = TaxUnit + label = "Vermont 2017 itemized deduction addback" + unit = USD + documentation = ( + "Federal itemized deductions Vermont adds back to 2017 federal " + "taxable income, following Schedule IN-155: deducted state and local " + "income taxes, up to the itemized deductions above the federal " + "standard deduction, plus other itemized deductions (excluding " + "medical and charitable) above 2.5 times the federal standard " + "deduction. Itemized deductions include wagering losses and are after " + "the section 68 limitation, which also scales the income taxes and " + "charity." + ) + definition_period = YEAR + defined_for = StateCode.VT + reference = ( + # Former 32 V.S.A. § 5811(21)(A)(iii)-(iv), reproduced in 2017 Vt. + # Acts No. 73, § 13a. PDF pages 21-22. + "https://legislature.vermont.gov/Documents/2018/Docs/ACTS/ACT073/ACT073%20As%20Enacted.pdf#page=21", + # 2017 Schedule IN-155 and the Department's 2017 worksheet for limited + # itemized deductions. + "https://web.archive.org/web/20250629013817/https://tax.vermont.gov/sites/tax/files/documents/IN-155-2017.pdf", + "https://web.archive.org/web/20201019064237/https://tax.vermont.gov/sites/tax/files/documents/2017%20IN-155%20Worksheet.xlsx", + ) + + def formula_2017(tax_unit, period, parameters): + p = parameters(period).gov.states.vt.tax.income.deductions.itemized_addback + itemizes = tax_unit("tax_unit_itemizes", period) + # IN-155 line 1 takes Schedule A line 29: allowed itemized deductions + # after the section 68 limitation, including wagering losses, which + # itemized_taxable_income_deductions leaves out. + itemized = add( + tax_unit, + period, + ["itemized_taxable_income_deductions", "wagering_losses_deduction"], + ) + standard = tax_unit("standard_deduction", period) + # The SALT deduction holds income taxes only when they exceed sales + # taxes (state_and_local_sales_or_income_tax takes the larger). + income_taxes = add( + tax_unit, period, ["state_withheld_income_tax", "local_income_tax"] + ) + sales_taxes = add(tax_unit, period, ["state_sales_tax", "local_sales_tax"]) + salt = tax_unit("salt_deduction", period) + deducted_income_taxes = where( + income_taxes >= sales_taxes, min_(income_taxes, salt), 0 + ) + # The Department's worksheet for limited itemized deductions scales + # income taxes and charity by the share of the limited deductions that + # section 68 leaves. Section 68(c) doesn't limit medical expenses. + limitation = parameters(period).gov.irs.deductions.itemized.limitation + total = tax_unit("total_itemized_taxable_income_deductions", period) + excluded = add(tax_unit, period, limitation.excluded_deductions) + limited = max_(total - excluded, 0) + reduction = tax_unit("itemized_taxable_income_deductions_reduction", period) + remaining_share = 1 - np.divide( + reduction, limited, out=np.zeros_like(limited), where=limited > 0 + ) + allowed_income_taxes = deducted_income_taxes * remaining_share + allowed_charitable = tax_unit("charitable_deduction", period) * remaining_share + medical = tax_unit("medical_expense_deduction", period) + income_tax_addback = min_(allowed_income_taxes, max_(itemized - standard, 0)) + other_deductions = ( + itemized - allowed_income_taxes - medical - allowed_charitable + ) + other_addback = max_(other_deductions - p.other_multiple * standard, 0) + return where(itemizes, income_tax_addback + other_addback, 0) + + def formula_2018(tax_unit, period, parameters): + return 0 diff --git a/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_other_additions.py b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_other_additions.py new file mode 100644 index 00000000000..ce2b2e8b263 --- /dev/null +++ b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_other_additions.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class vt_2017_other_additions(Variable): + value_type = float + entity = TaxUnit + label = "Vermont 2017 additions to federal taxable income" + unit = USD + documentation = ( + "Vermont's 2017 additions to federal taxable income other than the " + "itemized deduction addback: interest on non-Vermont state and local " + "obligations and the adjustment for federal bonus depreciation under " + "26 U.S.C. § 168(k). As in Vermont's AGI additions, all tax-exempt " + "interest is added; vt_other_additions carries the rest." + ) + definition_period = YEAR + defined_for = StateCode.VT + reference = ( + # Former 32 V.S.A. § 5811(21), as amended by 2015 Vt. Acts No. 57, § 64. + "https://legislature.vermont.gov/Documents/2016/Docs/ACTS/ACT057/ACT057%20As%20Enacted.pdf#page=89", + ) + + adds = ["tax_exempt_interest_income", "vt_other_additions"] diff --git a/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_other_subtractions.py b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_other_subtractions.py new file mode 100644 index 00000000000..8c6af95d4ac --- /dev/null +++ b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_other_subtractions.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class vt_2017_other_subtractions(Variable): + value_type = float + entity = TaxUnit + label = "Vermont 2017 other subtractions from federal taxable income" + unit = USD + documentation = ( + "Vermont's 2017 subtractions from federal taxable income that " + "PolicyEngine does not calculate, entered as a positive amount: the " + "capital gains exclusion under the law then in effect and the " + "depreciation adjustment for property whose federal bonus " + "depreciation was added back in an earlier year." + ) + definition_period = YEAR + defined_for = StateCode.VT + reference = ( + # Former 32 V.S.A. § 5811(21), as amended by 2015 Vt. Acts No. 57, § 64. + "https://legislature.vermont.gov/Documents/2016/Docs/ACTS/ACT057/ACT057%20As%20Enacted.pdf#page=89", + ) diff --git a/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_subtractions.py b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_subtractions.py new file mode 100644 index 00000000000..4ce44f8e6eb --- /dev/null +++ b/policyengine_us/variables/gov/states/vt/tax/income/deductions/vt_2017_subtractions.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class vt_2017_subtractions(Variable): + value_type = float + entity = TaxUnit + label = "Vermont 2017 subtractions from federal taxable income" + unit = USD + documentation = ( + "Vermont's 2017 subtractions from federal taxable income: income from " + "U.S. obligations, taxable state and local income tax refunds, and " + "other subtractions such as the capital gains exclusion and " + "depreciation adjustments for property placed in service earlier." + ) + definition_period = YEAR + defined_for = StateCode.VT + reference = ( + # Former 32 V.S.A. § 5811(21), as amended by 2015 Vt. Acts No. 57, § 64. + "https://legislature.vermont.gov/Documents/2016/Docs/ACTS/ACT057/ACT057%20As%20Enacted.pdf#page=89", + ) + + adds = ["us_govt_interest", "salt_refund_income", "vt_2017_other_subtractions"] diff --git a/policyengine_us/variables/gov/states/vt/tax/income/vt_taxable_income.py b/policyengine_us/variables/gov/states/vt/tax/income/vt_taxable_income.py index 771e9c3949e..43da19a2ee7 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/vt_taxable_income.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/vt_taxable_income.py @@ -6,13 +6,44 @@ class vt_taxable_income(Variable): entity = TaxUnit label = "Vermont taxable income" unit = USD - documentation = "VT AGI less taxable income deductions and exemptions" + documentation = ( + "Before 2017 and from 2018, Vermont AGI less the Vermont standard " + "deduction and personal exemptions. In 2017, federal taxable income with Vermont's " + "additions and subtractions under former 32 V.S.A. § 5811(21)." + ) definition_period = YEAR - reference = "https://tax.vermont.gov/sites/tax/files/documents/IN-111-2022.pdf" + reference = ( + "https://tax.vermont.gov/sites/tax/files/documents/IN-111-2022.pdf", + # Former 32 V.S.A. § 5811(21), as amended by 2015 Vt. Acts No. 57, § 64. + "https://legislature.vermont.gov/Documents/2016/Docs/ACTS/ACT057/ACT057%20As%20Enacted.pdf#page=89", + # 2017 Vt. Acts No. 73, § 13a reproduces the former text; § 32(5) + # changes the base from 2018. PDF pages 21-22, 59. + "https://legislature.vermont.gov/Documents/2018/Docs/ACTS/ACT073/ACT073%20As%20Enacted.pdf#page=21", + "https://web.archive.org/web/2018/https://tax.vermont.gov/sites/tax/files/documents/IN-111%20Instr-2017.pdf#page=5", + ) defined_for = StateCode.VT - def formula(tax_unit, period, parameters): + def formula_2017(tax_unit, period, parameters): + # Federal taxable income before its zero floor, so that a negative + # federal base offsets Vermont's additions. + agi = tax_unit("adjusted_gross_income", period) + exemptions = tax_unit("exemptions", period) + deductions = tax_unit("taxable_income_deductions", period) + federal_base = agi - exemptions - deductions + additions = add( + tax_unit, + period, + ["vt_2017_itemized_deduction_addback", "vt_2017_other_additions"], + ) + subtractions = tax_unit("vt_2017_subtractions", period) + return max_(0, federal_base + additions - subtractions) + + def formula_2018(tax_unit, period, parameters): agi = tax_unit("vt_agi", period) deductions = tax_unit("vt_standard_deduction", period) exemptions = tax_unit("vt_personal_exemptions", period) return max_(0, agi - deductions - exemptions) + + # Before 2017, keep the Vermont-base formula. Former § 5811(21) also set + # the federal base for 2015 and 2016, which this variable doesn't model. + formula = formula_2018