From ff8b5325905600d35973e620c2c607506d71eae7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 19:09:46 -0400 Subject: [PATCH 01/12] Take the 2020 charitable deduction for non-itemizers above the line CARES Act sec. 2204 added 26 U.S.C. 62(a)(22) for taxable years beginning in 2020: up to $300 ($150 married filing separately) of cash charitable contributions by an individual who does not itemize, deducted on 2020 Form 1040 line 10b in arriving at AGI. The model took it from AGI with the 2021 170(p) deduction instead. - New charitable_contributions_ald in the 2020 above-the-line list, so AGI and every MAGI built from the list (Social Security, UC exclusion, IRA and student loan worksheets, which all subtract line 10b) see it. - tax_unit_itemizes takes the itemized and standard deductions it compares from the branch with each choice in 2020, since AGI now depends on the choice. - non_itemizers_amount (170(p)) is zero in 2020. - charitable_deduction_for_non_itemizers is no longer listed among itemized deductions for 2020 and 2021, where it was added on top of the full charitable deduction once charitable_deduction stopped netting it. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-2020-charitable-ald.fixed.md | 1 + .../irs/ald/charitable_contributions/cap.yaml | 45 +++++++++++++++++++ .../parameters/gov/irs/ald/deductions.yaml | 19 ++++++++ .../deductions_if_not_itemizing.yaml | 6 +++ .../charity/non_itemizers_amount.yaml | 14 +----- .../irs/deductions/itemized_deductions.yaml | 15 ------- .../charitable_contributions_ald.py | 35 +++++++++++++++ .../charitable_deduction_for_non_itemizers.py | 12 ++++- .../deductions/tax_unit_itemizes.py | 41 ++++++++++++----- 9 files changed, 149 insertions(+), 39 deletions(-) create mode 100644 changelog.d/fix-2020-charitable-ald.fixed.md create mode 100644 policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions_ald.py diff --git a/changelog.d/fix-2020-charitable-ald.fixed.md b/changelog.d/fix-2020-charitable-ald.fixed.md new file mode 100644 index 00000000000..7b6db27f7f9 --- /dev/null +++ b/changelog.d/fix-2020-charitable-ald.fixed.md @@ -0,0 +1 @@ +Take the 2020 charitable deduction for non-itemizers (IRC 62(a)(22), Form 1040 line 10b) above the line, so it reduces adjusted gross income, and stop adding the 2020 and 2021 non-itemizer amount to itemized deductions. diff --git a/policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml b/policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml new file mode 100644 index 00000000000..f1d3d640582 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml @@ -0,0 +1,45 @@ +description: The IRS caps the above-the-line deduction for cash charitable contributions by individuals who do not itemize at this amount, based on filing status. +metadata: + propagate_metadata_to_children: true + unit: currency-USD + period: year + label: Above-the-line charitable deduction cap for non-itemizers + breakdown: + - filing_status + reference: + # CARES Act sec. 2204 added 26 U.S.C. 62(a)(22) and 62(f) for taxable + # years beginning in 2020: "the amount (not to exceed $300) of qualified + # charitable contributions made by an eligible individual". + - title: Pub. L. 116-136, sec. 2204 (CARES Act) + href: https://www.congress.gov/116/plaws/publ136/PLAW-116publ136.pdf#page=65 + # Pub. L. 116-260, div. EE, sec. 212(c) struck 62(a)(22) and 62(f) for + # taxable years beginning after 2020; sec. 212(a) replaced them with the + # below-the-line 26 U.S.C. 170(p). + - title: "Pub. L. 116-260, div. EE, sec. 212 (Consolidated Appropriations Act, 2021) #page 1888-1889" + href: https://www.congress.gov/116/plaws/publ260/PLAW-116publ260.pdf#page=1888 + # Form 1040 line 10b: $300 if single, head of household or qualifying + # widow(er), $300 if married filing jointly, $150 if married filing + # separately. + - title: 2020 Instructions for Form 1040, line 10b + href: https://www.irs.gov/pub/irs-prior/i1040gi--2020.pdf#page=30 + +SINGLE: + 2000-01-01: 0 + 2020-01-01: 300 + 2021-01-01: 0 +JOINT: + 2000-01-01: 0 + 2020-01-01: 300 + 2021-01-01: 0 +SEPARATE: + 2000-01-01: 0 + 2020-01-01: 150 + 2021-01-01: 0 +HEAD_OF_HOUSEHOLD: + 2000-01-01: 0 + 2020-01-01: 300 + 2021-01-01: 0 +SURVIVING_SPOUSE: + 2000-01-01: 0 + 2020-01-01: 300 + 2021-01-01: 0 diff --git a/policyengine_us/parameters/gov/irs/ald/deductions.yaml b/policyengine_us/parameters/gov/irs/ald/deductions.yaml index a2ecf812a21..d470d7041c9 100644 --- a/policyengine_us/parameters/gov/irs/ald/deductions.yaml +++ b/policyengine_us/parameters/gov/irs/ald/deductions.yaml @@ -33,6 +33,25 @@ values: - us_bonds_for_higher_ed - specified_possession_income - puerto_rico_income + # 26 U.S.C. 62(a)(22) (CARES Act sec. 2204) adds the charitable deduction + # for non-itemizers in 2020 only. + 2020-01-01: + - loss_ald + - self_employment_tax_ald + - student_loan_interest_ald + - early_withdrawal_penalty + - alimony_expense_ald + - educator_expense_ald + - capped_qualified_tuition_expenses_ald + - health_savings_account_ald + - self_employed_health_insurance_ald + - self_employed_pension_contribution_ald + - traditional_ira_deduction + - qualified_adoption_assistance_expense + - us_bonds_for_higher_ed + - specified_possession_income + - puerto_rico_income + - charitable_contributions_ald 2021-01-01: - loss_ald - self_employment_tax_ald diff --git a/policyengine_us/parameters/gov/irs/deductions/deductions_if_not_itemizing.yaml b/policyengine_us/parameters/gov/irs/deductions/deductions_if_not_itemizing.yaml index 454beae177b..fc58295ca71 100644 --- a/policyengine_us/parameters/gov/irs/deductions/deductions_if_not_itemizing.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/deductions_if_not_itemizing.yaml @@ -4,7 +4,13 @@ values: - standard_deduction - qualified_business_income_deduction - tuition_and_fees_deduction + # The 2020 charitable deduction for non-itemizers is above the line + # (charitable_contributions_ald). From 2021 it is taken from adjusted gross + # income under 26 U.S.C. 170(p) and 63(b)(4). 2020-01-01: + - standard_deduction + - qualified_business_income_deduction + 2021-01-01: - charitable_deduction_for_non_itemizers - standard_deduction - qualified_business_income_deduction diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/charity/non_itemizers_amount.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/charity/non_itemizers_amount.yaml index 7b100b2c0d6..e95802db011 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized/charity/non_itemizers_amount.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/charity/non_itemizers_amount.yaml @@ -7,11 +7,8 @@ metadata: breakdown: - filing_status reference: - - title: 2020 Publication 526 Charitable Contributions - href: https://www.irs.gov/pub/irs-prior/p526--2020.pdf#page=2 - title: 2021 Publication 526 Charitable Contributions href: https://www.irs.gov/pub/irs-prior/p526--2021.pdf#page=2 - # The legal code does not describe the 2020 deduction values - title: 26 U.S. Code § 170 - Charitable, etc., contributions and gifts - (p) href: https://www.law.cornell.edu/uscode/text/26/170 - title: Expanded tax benefits help individuals and businesses give to charity during 2021; deductions up to $600 available for cash donations by non-itemizers @@ -19,37 +16,30 @@ metadata: - title: H.R.1 - One Big Beautiful Bill Act href: https://www.congress.gov/bill/119th-congress/house-bill/1/text +# 26 U.S.C. 170(p) starts in 2021. The 2020 deduction for non-itemizers was +# above the line (26 U.S.C. 62(a)(22)); see gov.irs.ald.charitable_contributions. JOINT: 2018-01-01: 0 - 2020-01-01: 300 2021-01-01: 600 2022-01-01: 0 2026-01-01: 2_000 SINGLE: 2018-01-01: 0 - # CARES Act §2204: $300 cap for non-itemizing individuals in 2020 - # (IRS Pub. 526 (2020), p. 2). - 2020-01-01: 300 2021-01-01: 300 2022-01-01: 0 2026-01-01: 1_000 SEPARATE: 2018-01-01: 0 - # MFS 2020 is $150 per IRS guidance — a MFS couple shares the $300 - # joint cap ($150 each). - 2020-01-01: 150 2021-01-01: 300 2022-01-01: 0 2026-01-01: 1_000 HEAD_OF_HOUSEHOLD: 2018-01-01: 0 - 2020-01-01: 300 2021-01-01: 300 2022-01-01: 0 2026-01-01: 1_000 SURVIVING_SPOUSE: 2018-01-01: 0 - 2020-01-01: 300 2021-01-01: 300 2022-01-01: 0 2026-01-01: 1_000 diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml index 14d369f6999..55972586fa8 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml @@ -7,21 +7,6 @@ values: - medical_expense_deduction - casualty_loss_deduction - misc_deduction - 2020-01-01: - - charitable_deduction - - charitable_deduction_for_non_itemizers - - interest_deduction - - salt_deduction - - medical_expense_deduction - - casualty_loss_deduction - - misc_deduction - 2022-01-01: - - charitable_deduction - - interest_deduction - - salt_deduction - - medical_expense_deduction - - casualty_loss_deduction - - misc_deduction metadata: unit: list label: itemized deductions diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions_ald.py new file mode 100644 index 00000000000..9e59e9abf71 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions_ald.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class charitable_contributions_ald(Variable): + value_type = float + entity = TaxUnit + label = "Above-the-line charitable deduction for non-itemizers" + unit = USD + documentation = ( + "Above-the-line deduction for cash charitable contributions by a tax " + "unit that does not itemize (26 U.S.C. 62(a)(22), 2020 Form 1040 line " + "10b), which reduces adjusted gross income. It applies to taxable " + "years beginning in 2020 only; the 2021 and later deductions for " + "non-itemizers (26 U.S.C. 170(p)) are taken from adjusted gross " + "income in charitable_deduction_for_non_itemizers. Cash donations " + "are not split by recipient, so contributions to supporting " + "organizations and donor advised funds, which 62(f)(2) excludes, " + "are counted." + ) + definition_period = YEAR + reference = ( + # CARES Act sec. 2204: 26 U.S.C. 62(a)(22) and 62(f). + "https://www.congress.gov/116/plaws/publ136/PLAW-116publ136.pdf#page=65", + # 2020 Instructions for Form 1040, line 10b. + "https://www.irs.gov/pub/irs-prior/i1040gi--2020.pdf#page=30", + ) + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.ald.charitable_contributions + # 62(f)(1): an eligible individual is one who does not elect to + # itemize deductions. + itemizes = tax_unit("tax_unit_itemizes", period) + cash_donations = add(tax_unit, period, ["charitable_cash_donations"]) + filing_status = tax_unit("filing_status", period) + return ~itemizes * min_(cash_donations, p.cap[filing_status]) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.py index 8d627553d54..b7d0a30929c 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.py @@ -6,9 +6,17 @@ class charitable_deduction_for_non_itemizers(Variable): entity = TaxUnit label = "Charitable deduction for non-itemizers" unit = USD - documentation = "Charitable deduction amount for non-itemizers." + documentation = ( + "Charitable deduction for non-itemizers under 26 U.S.C. 170(p), taken " + "from adjusted gross income (2021 Form 1040 line 12b, and from 2026). " + "The 2020 deduction for non-itemizers is above the line: " + "charitable_contributions_ald." + ) definition_period = YEAR - reference = "https://www.irs.gov/newsroom/expanded-tax-benefits-help-individuals-and-businesses-give-to-charity-during-2021-deductions-up-to-600-available-for-cash-donations-by-non-itemizers" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/170#p", + "https://www.irs.gov/newsroom/expanded-tax-benefits-help-individuals-and-businesses-give-to-charity-during-2021-deductions-up-to-600-available-for-cash-donations-by-non-itemizers", + ) def formula(tax_unit, period, parameters): p = parameters(period).gov.irs.deductions.itemized.charity diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py index ab7736c1835..6909b31896e 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py @@ -1,6 +1,17 @@ from policyengine_us.model_api import * +def choice_branch(tax_unit, period, itemizes): + """The branch of the simulation in which every tax unit itemizes, or + every tax unit does not.""" + return get_override_branch( + tax_unit.simulation, + "itemizing" if itemizes else "not_itemizing", + period, + {"tax_unit_itemizes": np.full(tax_unit.count, itemizes, dtype=bool)}, + ) + + class tax_unit_itemizes(Variable): value_type = bool entity = TaxUnit @@ -10,9 +21,24 @@ class tax_unit_itemizes(Variable): definition_period = YEAR def formula(tax_unit, period, parameters): - standard_deduction = tax_unit("standard_deduction", period) - limitation = parameters(period).gov.irs.deductions.itemized.limitation - if limitation.applies and limitation.obbb.applies: + p = parameters(period).gov.irs + limitation = p.deductions.itemized.limitation + if "charitable_contributions_ald" in p.ald.deductions: + # In 2020 a unit that does not itemize deducts charitable + # contributions above the line (26 U.S.C. 62(a)(22)), so adjusted + # gross income depends on the choice, and with it the itemized + # deductions it limits (the medical floor, the charitable + # ceiling) and any standard deduction that phases out with it. + # Each comes from the branch that makes that choice (the ones + # that also give the tax liabilities compared below). + itemized_deductions = choice_branch(tax_unit, period, True).calculate( + "itemized_taxable_income_deductions", period + ) + standard_deduction = choice_branch(tax_unit, period, False).calculate( + "standard_deduction", period + ) + elif limitation.applies and limitation.obbb.applies: + standard_deduction = tax_unit("standard_deduction", period) # The itemized deductions the unit would claim if it itemized. # The P.L. 119-21 section 68 limitation nets them of the # qualified business income deduction, which depends on the @@ -20,16 +46,11 @@ def formula(tax_unit, period, parameters): # (the one that also gives tax_liability_if_itemizing). # Calculated before branching, so the branch inherits it. tax_unit("total_itemized_taxable_income_deductions", period) - itemizing_branch = get_override_branch( - tax_unit.simulation, - "itemizing", - period, - {"tax_unit_itemizes": np.ones((tax_unit.count,), dtype=bool)}, - ) - itemized_deductions = itemizing_branch.calculate( + itemized_deductions = choice_branch(tax_unit, period, True).calculate( "itemized_taxable_income_deductions", period ) else: + standard_deduction = tax_unit("standard_deduction", period) itemized_deductions = tax_unit("itemized_taxable_income_deductions", period) if parameters(period).gov.simulation.branch_to_determine_itemization: # determine federal itemization behavior by comparing tax liability From 8564158a76230ea53c487b61d5105ddbbe6bdfcd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 19:31:12 -0400 Subject: [PATCH 02/12] Test the 2020 above-the-line charitable deduction from the 2020 Form 1040 Unit cases for each filing status cap, itemizers, noncash gifts, 2019 and 2021; integration cases worked from Form 1040 lines 9-15, the Social Security Benefits and Unemployment Compensation Exclusion worksheets, an itemization choice the deduction flips, and the 2020 and 2021 itemizers who no longer count the gifts twice. Co-Authored-By: Claude Opus 5.5 --- .../charitable_contributions_ald.yaml | 105 +++++++ .../charitable_contributions/integration.yaml | 261 ++++++++++++++++++ ...haritable_deduction_for_non_itemizers.yaml | 17 +- .../charitable_contributions_ald.py | 0 4 files changed, 374 insertions(+), 9 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml rename policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/{ => charitable_contributions}/charitable_contributions_ald.py (100%) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml new file mode 100644 index 00000000000..efbe526e7df --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml @@ -0,0 +1,105 @@ +# 26 U.S.C. 62(a)(22) and 62(f) (CARES Act sec. 2204), 2020 only. Caps from +# the 2020 Instructions for Form 1040, line 10b: $300 if single, head of +# household or qualifying widow(er), $300 if married filing jointly, $150 if +# married filing separately. + +- name: Case 1, 2020 non-itemizer below the cap deducts all cash gifts. + period: 2020 + input: + charitable_cash_donations: 200 + filing_status: SINGLE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 200 + +- name: Case 2, 2020 single non-itemizer exactly at the cap. + period: 2020 + input: + charitable_cash_donations: 300 + filing_status: SINGLE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 300 + +- name: Case 3, 2020 single non-itemizer above the cap. + period: 2020 + input: + charitable_cash_donations: 500 + filing_status: SINGLE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 300 + +- name: Case 4, 2020 joint return is capped at $300, not $600. + period: 2020 + input: + charitable_cash_donations: 700 + filing_status: JOINT + tax_unit_itemizes: false + output: + charitable_contributions_ald: 300 + +- name: Case 5, 2020 married filing separately is capped at $150. + period: 2020 + input: + charitable_cash_donations: 400 + filing_status: SEPARATE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 150 + +- name: Case 6, 2020 head of household is capped at $300. + period: 2020 + input: + charitable_cash_donations: 400 + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_itemizes: false + output: + charitable_contributions_ald: 300 + +- name: Case 7, 2020 qualifying widow(er) is capped at $300. + period: 2020 + input: + charitable_cash_donations: 400 + filing_status: SURVIVING_SPOUSE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 300 + +- name: Case 8, 2020 itemizer is not an eligible individual (62(f)(1)). + period: 2020 + input: + charitable_cash_donations: 500 + filing_status: SINGLE + tax_unit_itemizes: true + output: + charitable_contributions_ald: 0 + +- name: Case 9, noncash gifts do not qualify (62(f)(2)(A)). + period: 2020 + input: + charitable_cash_donations: 0 + charitable_non_cash_donations: 1_000 + filing_status: SINGLE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 0 + +- name: Case 10, no deduction in 2019, before 62(a)(22). + period: 2019 + input: + charitable_cash_donations: 500 + filing_status: SINGLE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 0 + +- name: Case 11, no above-the-line deduction in 2021; 170(p) takes it from AGI instead. + period: 2021 + input: + charitable_cash_donations: 500 + filing_status: SINGLE + tax_unit_itemizes: false + output: + charitable_contributions_ald: 0 + charitable_deduction_for_non_itemizers: 300 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml new file mode 100644 index 00000000000..43f6f09e63d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml @@ -0,0 +1,261 @@ +# Worked from the 2020 Form 1040 (lines 9, 10a-10c, 11, 12 and 15) and its +# instructions: line 10b (file page 30), the 2020 Tax Rate Schedules (file +# page 111), the Standard Deduction Chart for People Born Before January 2, +# 1956 (file page 33), the Social Security Benefits Worksheet (file page 31, +# line 6 subtracts line 10b) and the Unemployment Compensation Exclusion +# Worksheet (file page 88, line 5 subtracts line 10c). +# https://www.irs.gov/pub/irs-prior/i1040gi--2020.pdf +# New Hampshire taxes no wages and has no sales tax, so there is no state and +# local tax deduction. + +- name: Case 1, 2020 single non-itemizer deducts $300 of $500 in cash gifts above the line. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + # Line 9: total income. + irs_gross_income: 40_000 + # Line 10b: min(500, 300); line 10c = 10a + 10b. + charitable_contributions_ald: 300 + above_the_line_deductions: 300 + # Line 11: 40,000 - 300. + adjusted_gross_income: 39_700 + tax_unit_itemizes: false + # Line 12: 2020 single standard deduction; nothing below the line for + # the charitable gifts. + standard_deduction: 12_400 + charitable_deduction_for_non_itemizers: 0 + taxable_income_deductions: 12_400 + # Line 15: 39,700 - 12,400. + taxable_income: 27_300 + # Schedule X: 987.50 + 12% x (27,300 - 9,875). + income_tax_main_rates: 3_078.5 + +- name: Case 2, 2020 joint return deducts $300, not $600. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 80_000 + charitable_cash_donations: 1_000 + person2: + age: 40 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NH + output: + charitable_contributions_ald: 300 + # Line 11: 80,000 - 300. + adjusted_gross_income: 79_700 + standard_deduction: 24_800 + # Line 15: 79,700 - 24,800. + taxable_income: 54_900 + # Schedule Y-1: 1,975 + 12% x (54,900 - 19,750). + income_tax_main_rates: 6_193 + +- name: Case 3, 2020 married filing separately deducts $150. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 50_000 + charitable_cash_donations: 400 + tax_units: + tax_unit: + members: [person1] + filing_status: SEPARATE + households: + household: + members: [person1] + state_code: NH + output: + charitable_contributions_ald: 150 + # Line 11: 50,000 - 150. + adjusted_gross_income: 49_850 + standard_deduction: 12_400 + # Line 15: 49,850 - 12,400. + taxable_income: 37_450 + # Schedule Y-2: 987.50 + 12% x (37,450 - 9,875). + income_tax_main_rates: 4_296.5 + +- name: Case 4, 2020 itemizer deducts the gifts on Schedule A only, once. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 100_000 + deductible_mortgage_interest: 20_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + tax_unit_itemizes: true + # Line 10b is for those who take the standard deduction. + charitable_contributions_ald: 0 + adjusted_gross_income: 100_000 + charitable_deduction: 500 + # Schedule A: 20,000 interest + 500 gifts. + total_itemized_taxable_income_deductions: 20_500 + # Line 15: 100,000 - 20,500. + taxable_income: 79_500 + +- name: Case 5, 2020 line 10b makes the standard deduction the better choice. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 60_000 + deductible_mortgage_interest: 12_300 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + # Itemizing: line 11 = 60,000, Schedule A = 12,300 + 300 = 12,600, line + # 15 = 47,400. Standard deduction: line 11 = 60,000 - 300 = 59,700, line + # 15 = 59,700 - 12,400 = 47,300, so taking the standard deduction leaves + # $100 less taxable income although Schedule A exceeds $12,400. + tax_unit_itemizes: false + charitable_contributions_ald: 300 + adjusted_gross_income: 59_700 + taxable_income: 47_300 + # Schedule X: 4,617.50 + 22% x (47,300 - 40,125). + income_tax_main_rates: 6_196 + +- name: Case 6, 2020 line 10b lowers the income that makes Social Security benefits taxable. + period: 2020 + input: + people: + person1: + age: 70 + social_security_retirement: 20_000 + taxable_pension_income: 20_000 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + charitable_contributions_ald: 300 + # Social Security Benefits Worksheet: line 3 = 20,000 (line 5b), line 6 + # = 300 (line 10b), line 7 = 10,000 + 20,000 - 300 = 29,700. + taxable_ss_magi: 19_700 + tax_unit_combined_income_for_social_security_taxability: 29_700 + # Line 9 = 29,700 - 25,000 = 4,700; line 12 = min(4,700, 9,000); line 13 + # = 2,350; line 14 = min(10,000, 2,350); line 15 = 0; line 18 = + # min(2,350, 17,000). + tax_unit_taxable_social_security: 2_350 + # Line 11: 20,000 + 2,350 - 300. + adjusted_gross_income: 22_050 + # Single, born before January 2, 1956: $14,050. + standard_deduction: 14_050 + # Line 15: 22,050 - 14,050. + taxable_income: 8_000 + income_tax_main_rates: 800 + +- name: Case 7, 2020 line 10c brings income under the unemployment compensation exclusion limit. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 150_200 + unemployment_compensation: 5_000 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + charitable_contributions_ald: 300 + # Unemployment Compensation Exclusion Worksheet: line 4 = 150,200, line + # 5 = 300 (line 10c), line 6 = 149,900, under $150,000, so line 10 + # excludes all 5,000. + taxable_uc_agi: 149_900 + tax_unit_taxable_unemployment_compensation: 0 + # Line 11: 150,200 + 0 - 300. + adjusted_gross_income: 149_900 + +- name: Case 8, 2021 itemizer deducts the gifts on Schedule A only, once. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 100_000 + deductible_mortgage_interest: 20_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + tax_unit_itemizes: true + adjusted_gross_income: 100_000 + # Schedule A: 20,000 interest + 500 gifts; the 170(p) amount is for + # those who take the standard deduction. + total_itemized_taxable_income_deductions: 20_500 + taxable_income: 79_500 + +- name: Case 9, 2021 non-itemizer deducts the gifts after AGI, on line 12b. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + charitable_contributions_ald: 0 + # 2021 line 11. + adjusted_gross_income: 40_000 + tax_unit_itemizes: false + # Line 12b: min(500, 300); line 12c = 12,550 + 300. + charitable_deduction_for_non_itemizers: 300 + taxable_income_deductions: 12_850 + # Line 15: 40,000 - 12,850. + taxable_income: 27_150 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.yaml index a13cf383449..c189aa85d44 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction_for_non_itemizers.yaml @@ -6,32 +6,31 @@ output: charitable_deduction_for_non_itemizers: 0 -- name: 2020 single filer more than limit +- name: 2020 single filer, whose deduction is above the line instead period: 2020 input: charitable_cash_donations: 400 filing_status: SINGLE - # CARES Act §2204 set the non-itemizer cap at $300 for 2020, not $150. - # Per IRS Pub. 526 (2020) p.2, MFS was limited to $150 (sharing the - # joint $300 cap), but SINGLE/HoH/SURVIVING_SPOUSE get the full $300. + # 170(p) starts in 2021. The 2020 deduction for non-itemizers is the + # above-the-line charitable_contributions_ald (26 U.S.C. 62(a)(22)). output: - charitable_deduction_for_non_itemizers: 300 + charitable_deduction_for_non_itemizers: 0 -- name: 2020 married filers more than limit +- name: 2020 married filers, whose deduction is above the line instead period: 2020 input: charitable_cash_donations: 700 filing_status: JOINT output: - charitable_deduction_for_non_itemizers: 300 + charitable_deduction_for_non_itemizers: 0 -- name: 2020 MFS filer capped at $150 (half of joint $300) +- name: 2020 MFS filer, whose deduction is above the line instead period: 2020 input: charitable_cash_donations: 400 filing_status: SEPARATE output: - charitable_deduction_for_non_itemizers: 150 + charitable_deduction_for_non_itemizers: 0 - name: 2021 single filer less than limit period: 2021 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py similarity index 100% rename from policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions_ald.py rename to policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py From fbda2bd2fff4758d7632a67e3d09de8388208437 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 19:49:26 -0400 Subject: [PATCH 03/12] Keep the 2020 charitable deduction out of state AGI where states decoupled Taking the deduction above the line moves it into every state AGI that starts from federal AGI. States that did not adopt 62(a)(22) for 2020 add it back: - California: Schedule CA (540) Part I, Section C, line 22, column B. - Indiana: IC 6-3-1-3.5(a)(26), Information Bulletin 119 (code 120). - Kentucky: Schedule M line 5, by each spouse's share of federal AGI. - Minnesota: Schedule M1NC line 6, through M1M line 16. - North Carolina: D-400 Schedule S, Part A, line 16. - New York: IT-558 addition A-003 (recomputed federal AGI). Massachusetts Part B AGI, which subtracts the federal ALD list, disallows it (TIR 20-9, B(6)). Arizona's 2020 standard deduction increase for charitable contributions subtracts it (worksheet line 5C). charitable_contributions_ald returns 0 in years that do not list it, so these additions never reach the itemization choice outside 2020. Co-Authored-By: Claude Opus 5.5 --- .../states/ma/tax/income/ald/disallowed.yaml | 6 ++ .../mn/tax/income/additions/sources.yaml | 7 ++ ...eduction_for_charitable_contributions.yaml | 43 ++++++++++++ .../states/ca/tax/income/ca_additions.yaml | 61 +++++++++++++++++ .../in/tax/income/add_backs/in_add_backs.yaml | 39 +++++++++++ .../states/ky/tax/income/ky_additions.yaml | 65 +++++++++++++++++++ .../adjusted_gross_income/ma_part_b_agi.yaml | 22 +++++++ .../mn/tax/income/additions/mn_additions.yaml | 39 +++++++++++ .../states/nc/tax/income/nc_additions.yaml | 39 +++++++++++ .../adjusted_gross_income/ny_additions.yaml | 41 ++++++++++++ .../charitable_contributions_ald.py | 9 +++ ..._deduction_for_charitable_contributions.py | 17 ++++- .../gov/states/ca/tax/income/ca_additions.py | 13 +++- .../in/tax/income/add_backs/in_add_backs.py | 8 ++- .../gov/states/ky/tax/income/ky_additions.py | 14 ++++ .../gov/states/nc/tax/income/nc_additions.py | 7 ++ .../adjusted_gross_income/ny_additions.py | 11 ++-- 17 files changed, 432 insertions(+), 9 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/in/tax/income/add_backs/in_add_backs.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/additions/mn_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml diff --git a/policyengine_us/parameters/gov/states/ma/tax/income/ald/disallowed.yaml b/policyengine_us/parameters/gov/states/ma/tax/income/ald/disallowed.yaml index 7016ba2a218..0016a94c8cd 100644 --- a/policyengine_us/parameters/gov/states/ma/tax/income/ald/disallowed.yaml +++ b/policyengine_us/parameters/gov/states/ma/tax/income/ald/disallowed.yaml @@ -11,11 +11,15 @@ values: - early_withdrawal_penalty # (h) - self_employment_tax_ald - alimony_income # is a US ALD only in 2018 through 2025 + # Massachusetts follows Code 62(a) as in effect on January 1, 2005, so + # it does not adopt 62(a)(22) (TIR 20-9, B(6)). + - charitable_contributions_ald 2026-01-01: - loss_ald # (c) - traditional_ira_deduction # (f) - early_withdrawal_penalty # (h) - self_employment_tax_ald + - charitable_contributions_ald metadata: unit: list period: year @@ -23,3 +27,5 @@ metadata: reference: - title: Mass. General Laws c.62 § 2(d)(1)(A) href: https://www.mass.gov/info-details/mass-general-laws-c62-ss-2 + - title: Massachusetts DOR TIR 20-9, Massachusetts tax implications of the CARES Act, B(6) + href: https://archives.lib.state.ma.us/server/api/core/bitstreams/84d51bb2-c47f-4aa5-908a-b534fa72cabe/content#page=6 diff --git a/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml b/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml index 29d59f3f3f0..e1660f48708 100644 --- a/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml +++ b/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml @@ -1,5 +1,10 @@ description: Minnesota adds these variables to federal adjusted gross income when computing its taxable income. values: + # 2020 Schedule M1NC line 6 adds back the federal charitable deduction for + # non-itemizers (CARES Act sec. 2204), through Form M1M line 16. + 2020-01-01: + - form_4972_lumpsum_distributions + - charitable_contributions_ald 2021-01-01: - form_4972_lumpsum_distributions @@ -8,6 +13,8 @@ metadata: period: year label: Minnesota additions to federal AGI reference: + - title: 2020 Minnesota Schedule M1NC, Federal Adjustments + href: https://www.revenue.state.mn.us/sites/default/files/2024-02/m1nc_20.pdf#page=1 - title: 2021 Minnesota Schedule M1M, Income Additions and Subtractions, Page 3 href: https://www.taxformfinder.org/forms/2021/2021-minnesota-form-m1m.pdf#page=3 - title: 2022 Minnesota Schedule M1M, Income Additions and Subtractions, Page 3 diff --git a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.yaml b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.yaml index bb735ea8f16..330df92d435 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.yaml @@ -117,3 +117,46 @@ # charitable after credit = max($1,200 - $400, 0) = $800 # rate (0) * 800 + min($800, $1,000 single cap) = $800 az_increased_standard_deduction_for_charitable_contributions: 800 + +- name: test for 2020 federal non-itemizer, whose line 10b deduction is subtracted + period: 2020 + input: + state_code: AZ + charitable_deduction: 500 + charitable_contributions_ald: 300 + az_charitable_contributions_credit_potential: 0 + output: + # 2020 worksheet: line 4C 500, line 5C 300, line 6C 0; 25% of 200. + az_increased_standard_deduction_for_charitable_contributions: 50 + +- name: test for 2020 federal deduction and credit exceeding contributions + period: 2020 + input: + state_code: AZ + charitable_deduction: 500 + charitable_contributions_ald: 300 + az_charitable_contributions_credit_potential: 400 + output: + # max(500 - 300 - 400, 0) = 0 + az_increased_standard_deduction_for_charitable_contributions: 0 + +- name: test for 2020 household that gives $500 in cash and takes the federal standard deduction + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: AZ + output: + charitable_contributions_ald: 300 + charitable_deduction: 500 + # 25% of (500 - 300) + az_increased_standard_deduction_for_charitable_contributions: 50 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_additions.yaml new file mode 100644 index 00000000000..1262441653f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_additions.yaml @@ -0,0 +1,61 @@ +# 2020 Schedule CA (540) instructions, Part I, Section C, line 22, column B: +# the federal Form 1040 line 10b charitable deduction is added back. +# https://www.ftb.ca.gov/forms/2020/2020-540-booklet.pdf#page=48 + +- name: Case 1, 2020 federal non-itemizer adds back the line 10b deduction. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: CA + output: + # Federal AGI 40,000 - 300 = 39,700; California adds the 300 back. + adjusted_gross_income: 39_700 + ca_additions: 300 + ca_agi: 40_000 + +- name: Case 2, 2020 federal itemizer has no line 10b deduction to add back. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 100_000 + charitable_cash_donations: 500 + deductible_mortgage_interest: 20_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: CA + output: + ca_additions: 0 + +- name: Case 3, 2021 deduction for non-itemizers is below the line, so nothing is added back. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: CA + output: + ca_additions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/in/tax/income/add_backs/in_add_backs.yaml b/policyengine_us/tests/policy/baseline/gov/states/in/tax/income/add_backs/in_add_backs.yaml new file mode 100644 index 00000000000..b419fc8fd4a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/in/tax/income/add_backs/in_add_backs.yaml @@ -0,0 +1,39 @@ +# IC 6-3-1-3.5(a)(26) adds back the 2020 IRC 62(a)(22) charitable deduction +# (Information Bulletin 119, code 120). +# https://www.in.gov/dor/files/reference/ib119.pdf#page=2 + +- name: Case 1, 2020 federal non-itemizer adds back the line 10b deduction. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IN + output: + in_add_backs: 300 + +- name: Case 2, 2021 deduction for non-itemizers is below the line, so nothing is added back. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IN + output: + in_add_backs: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml new file mode 100644 index 00000000000..179c0d003c4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml @@ -0,0 +1,65 @@ +# 2020 Form 740 instructions, Schedule M line 5: contributions claimed on +# federal Form 1040 line 10b are added back. +# https://revenue.ky.gov/Forms/740%20Instructions%20Packet%205.7.21.pdf#page=27 + +- name: Case 1, 2020 single federal non-itemizer adds back the line 10b deduction. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: KY + output: + adjusted_gross_income_person: 39_700 + ky_additions: 300 + ky_agi: 40_000 + +- name: Case 2, 2020 joint federal non-itemizers add back half of the deduction each. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 50_000 + charitable_cash_donations: 1_000 + person2: + age: 40 + employment_income: 30_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: KY + output: + # Federal person AGI deducts half of the $300 from each spouse. + adjusted_gross_income_person: [49_850, 29_850] + ky_additions: [150, 150] + ky_agi: [50_000, 30_000] + +- name: Case 3, 2021 deduction for non-itemizers is below the line, so nothing is added back. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: KY + output: + ky_additions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml index e350f4cb091..293904a7c0c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml @@ -33,3 +33,25 @@ # insurance deduction is left out too: it is on the child's own return. ma_part_b_gross_income: 40_000 ma_part_b_agi: 40_000 + +- name: The 2020 federal charitable deduction for non-itemizers does not reduce Part B AGI + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MA + output: + # Massachusetts follows Code 62(a) as in effect on January 1, 2005, so + # it does not adopt 62(a)(22) (TIR 20-9, B(6)). + adjusted_gross_income: 39_700 + ma_part_b_agi: 40_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/additions/mn_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/additions/mn_additions.yaml new file mode 100644 index 00000000000..43c8a998b01 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/additions/mn_additions.yaml @@ -0,0 +1,39 @@ +# 2020 Schedule M1NC line 6 adds back the federal charitable deduction for +# non-itemizers, through Form M1M line 16. +# https://www.revenue.state.mn.us/sites/default/files/2024-02/m1nc_20.pdf#page=1 + +- name: Case 1, 2020 federal non-itemizer adds back the line 10b deduction. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MN + output: + mn_additions: 300 + +- name: Case 2, 2021 deduction for non-itemizers is below the line, so nothing is added back. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MN + output: + mn_additions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_additions.yaml new file mode 100644 index 00000000000..aca3dbcc078 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_additions.yaml @@ -0,0 +1,39 @@ +# 2020 D-400 Schedule S, Part A, line 16: above-the-line qualified charitable +# contribution deduction. +# https://www.ncdor.gov/documents/files/2020-d-400-schedule-s-web/open#page=2 + +- name: Case 1, 2020 federal non-itemizer adds back the line 10b deduction. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NC + output: + nc_additions: 300 + +- name: Case 2, 2021 deduction for non-itemizers is below the line, so nothing is added back. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NC + output: + nc_additions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml new file mode 100644 index 00000000000..68633227a96 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml @@ -0,0 +1,41 @@ +# 2020 Form IT-558 instructions, addition A-003: the IRC 62(a)(22) +# charitable deduction is added back to recompute federal AGI. +# https://www.tax.ny.gov/pdf/2020/inc/it558i_2020.pdf#page=2 + +- name: Case 1, 2020 federal non-itemizer adds back the line 10b deduction. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + output: + adjusted_gross_income: 39_700 + ny_additions: 300 + ny_agi: 40_000 + +- name: Case 2, 2021 deduction for non-itemizers is below the line, so nothing is added back. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + output: + ny_additions: 0 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py index 9e59e9abf71..c94095e9e11 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py @@ -26,6 +26,15 @@ class charitable_contributions_ald(Variable): ) def formula(tax_unit, period, parameters): + # Only in years that list it among above-the-line deductions, where + # tax_unit_itemizes takes the itemization choice from branches. + # State additions that read it then never reach the choice in other + # years. + if ( + "charitable_contributions_ald" + not in parameters(period).gov.irs.ald.deductions + ): + return 0 p = parameters(period).gov.irs.ald.charitable_contributions # 62(f)(1): an eligible individual is one who does not elect to # itemize deductions. diff --git a/policyengine_us/variables/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.py b/policyengine_us/variables/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.py index 2855d2b092e..0817fa10030 100644 --- a/policyengine_us/variables/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.py +++ b/policyengine_us/variables/gov/states/az/tax/income/deductions/standard/az_increased_standard_deduction_for_charitable_contributions.py @@ -6,7 +6,12 @@ class az_increased_standard_deduction_for_charitable_contributions(Variable): entity = TaxUnit label = "Arizona increased standard deduction for charitable contributions" unit = USD - reference = "https://www.azleg.gov/viewdocument/?docName=https://www.azleg.gov/ars/43/01041.htm" + reference = ( + "https://www.azleg.gov/viewdocument/?docName=https://www.azleg.gov/ars/43/01041.htm", + # 2020 Form 140 booklet, Standard Deduction Increase for Charitable + # Contributions worksheet, Note 1 and line 5C. + "https://azdor.gov/sites/default/files/2023-03/FORMS_INDIVIDUAL_2020_140BOOKLET.pdf#page=25", + ) definition_period = YEAR defined_for = StateCode.AZ @@ -16,8 +21,16 @@ def formula(tax_unit, period, parameters): charitable_contributions_credit = tax_unit( "az_charitable_contributions_credit_potential", period ) + # 2020 worksheet line 5C: contributions already deducted on the + # federal return by a filer who did not itemize (line 10b). + federal_non_itemizer_deduction = tax_unit( + "charitable_contributions_ald", period + ) charitable_deduction_after_credit = max_( - charitable_deduction - charitable_contributions_credit, 0 + charitable_deduction + - federal_non_itemizer_deduction + - charitable_contributions_credit, + 0, ) # Through TY2025, the add-on is a share of charitable contributions # (the rate). From TY2026 (HB 4168), the rate is zero and the add-on diff --git a/policyengine_us/variables/gov/states/ca/tax/income/ca_additions.py b/policyengine_us/variables/gov/states/ca/tax/income/ca_additions.py index 7bedfbf2c97..ac48aac3561 100644 --- a/policyengine_us/variables/gov/states/ca/tax/income/ca_additions.py +++ b/policyengine_us/variables/gov/states/ca/tax/income/ca_additions.py @@ -7,6 +7,15 @@ class ca_additions(Variable): label = "CA AGI additions to federal AGI" unit = USD definition_period = YEAR - reference = "https://www.ftb.ca.gov/forms/2021/2021-540.pdf" + reference = ( + "https://www.ftb.ca.gov/forms/2021/2021-540.pdf", + # 2020 Schedule CA (540) instructions, Part I, Section C, line 22, + # column B: the federal Form 1040 line 10b charitable deduction. + "https://www.ftb.ca.gov/forms/2020/2020-540-booklet.pdf#page=48", + ) defined_for = StateCode.CA - adds = ["ca_hsa_addition", "ca_educator_expense_addition"] + adds = [ + "ca_hsa_addition", + "ca_educator_expense_addition", + "charitable_contributions_ald", + ] diff --git a/policyengine_us/variables/gov/states/in/tax/income/add_backs/in_add_backs.py b/policyengine_us/variables/gov/states/in/tax/income/add_backs/in_add_backs.py index 0c5500e464c..ca650e09e63 100644 --- a/policyengine_us/variables/gov/states/in/tax/income/add_backs/in_add_backs.py +++ b/policyengine_us/variables/gov/states/in/tax/income/add_backs/in_add_backs.py @@ -7,10 +7,16 @@ class in_add_backs(Variable): label = "Indiana add-backs" unit = USD definition_period = YEAR - reference = "http://iga.in.gov/legislative/laws/2021/ic/titles/006#6-3-1-3.5" + reference = ( + "http://iga.in.gov/legislative/laws/2021/ic/titles/006#6-3-1-3.5", + # Information Bulletin 119: IC 6-3-1-3.5(a)(26) adds back the 2020 + # IRC 62(a)(22) charitable deduction (code 120). + "https://www.in.gov/dor/files/reference/ib119.pdf#page=2", + ) defined_for = StateCode.IN adds = [ + "charitable_contributions_ald", "in_bonus_depreciation_add_back", "in_nol_add_back", "in_oos_municipal_obligation_interest_add_back", diff --git a/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py b/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py index 94360c4773c..e5f3b3256a2 100644 --- a/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py +++ b/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py @@ -13,5 +13,19 @@ class ky_additions(Variable): "https://revenue.ky.gov/Forms/740%20Packet%20Instructions%205-9-23.pdf#page=23", "https://revenue.ky.gov/Forms/Schedule%20M-2021.pdf#page=1", "https://taxsim.nber.org/historical_state_tax_forms/KY/2021/Form%20740%20Packet%20Instructions-2021.pdf#page=27", + # 2020 Form 740 instructions, Schedule M line 5: contributions + # claimed on federal Form 1040 line 10b. + "https://revenue.ky.gov/Forms/740%20Instructions%20Packet%205.7.21.pdf#page=27", ) defined_for = StateCode.KY + + def formula(person, period, parameters): + # The 2020 federal charitable deduction for non-itemizers is added + # back in the share of it that adjusted_gross_income_person (and so + # ky_federal_agi) deducts from each filer: half each on a joint + # return. + filing_status = person.tax_unit("filing_status", period) + share = person("is_tax_unit_head_or_spouse", period) * where( + filing_status == filing_status.possible_values.JOINT, 0.5, 1.0 + ) + return share * person.tax_unit("charitable_contributions_ald", period) diff --git a/policyengine_us/variables/gov/states/nc/tax/income/nc_additions.py b/policyengine_us/variables/gov/states/nc/tax/income/nc_additions.py index b6c64319f72..19bfe4d767e 100644 --- a/policyengine_us/variables/gov/states/nc/tax/income/nc_additions.py +++ b/policyengine_us/variables/gov/states/nc/tax/income/nc_additions.py @@ -7,4 +7,11 @@ class nc_additions(Variable): label = "North Carolina additions to the adjusted gross income" unit = USD definition_period = YEAR + reference = ( + # 2020 D-400 Schedule S, Part A, line 16: above-the-line qualified + # charitable contribution deduction. + "https://www.ncdor.gov/documents/files/2020-d-400-schedule-s-web/open#page=2", + ) defined_for = StateCode.NC + + adds = ["charitable_contributions_ald"] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py index be0a5b3c66d..994f96aac03 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py @@ -8,10 +8,13 @@ class ny_additions(Variable): unit = USD documentation = "Additions to NY AGI over federal AGI." definition_period = YEAR - reference = dict( - title="N.Y. Comp. Codes R. & Regs. tit. 20 § 112.2", - href="https://www.law.cornell.edu/regulations/new-york/20-NYCRR-112.2", + reference = ( + "https://www.law.cornell.edu/regulations/new-york/20-NYCRR-112.2", + # 2020 Form IT-558 instructions, addition A-003: the IRC 62(a)(22) + # charitable deduction, which recomputes federal AGI (IT-201 line + # 19a). + "https://www.tax.ny.gov/pdf/2020/inc/it558i_2020.pdf#page=2", ) defined_for = StateCode.NY - # No additions modeled in PolicyEngine US. + adds = ["charitable_contributions_ald"] From ac50dc0f4f4b8819af56b5b1e8d1abe746ad9d60 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 19:56:04 -0400 Subject: [PATCH 04/12] Take the 2020 charitable deduction off Michigan household resources Michigan Treasury: for 2020 the cash contribution deduction is a deduction from AGI 'and therefore Total Household Resources as well'; for 2021 it is not. Line 30 now lists charitable_contributions_ald in 2020. Co-Authored-By: Claude Opus 5.5 --- .../household_resources_adjustments.yaml | 18 ++++++++ .../mi/tax/income/mi_household_resources.yaml | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml index 94637b89d28..aff50d4db09 100644 --- a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml +++ b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml @@ -7,6 +7,20 @@ values: # items) and the exclusions from gross income under IRC 135, 137, 931 and # 933 (us_bonds_for_higher_ed, qualified_adoption_assistance_expense, # specified_possession_income and puerto_rico_income). + # In 2020 the charitable deduction for non-itemizers (U.S. Form 1040 line + # 10b) is a deduction from adjusted gross income "and therefore Total + # Household Resources as well" (Michigan Treasury FAQ). + 2020-01-01: + - educator_expense_ald # line 11, capped per eligible educator + - health_savings_account_ald # line 13 + - self_employment_tax_ald # line 15 + - self_employed_pension_contribution_ald # line 16 + - self_employed_health_insurance_ald # line 17 + - early_withdrawal_penalty # line 18 + - alimony_expense_ald # line 19a + - traditional_ira_deduction # line 20, the deductible amount + - student_loan_interest_ald # line 21 + - charitable_contributions_ald 2021-01-01: - educator_expense_ald # line 11, capped per eligible educator - health_savings_account_ald # line 13 @@ -22,6 +36,10 @@ metadata: period: year label: Michigan household resources adjustments reference: + - title: Michigan 2020 Individual Income Tax Instructions, MI-1040CR line 30 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2020/Book_MI-1040_instructions_only.pdf#page=32 + - title: Michigan Treasury FAQ, charitable contribution deductions from AGI and total household resources (2020 and 2021) + href: https://www.michigan.gov/taxes/questions/iit/accordion/covid19-cares/charitable-contribution-deduct556422 - title: Michigan 2024 Individual Income Tax Instructions, MI-1040CR line 30 href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2024/MI-1040-Instructions.pdf#page=33 - title: Michigan 2025 Individual Income Tax Instructions, MI-1040CR line 30 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml index 9e9345ba1a7..83f3615cf3a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml @@ -1098,3 +1098,46 @@ output: # Line 33: 10,000 - 1,000 = 9,000. mi_household_resources: 9_000 + +- name: The 2020 charitable deduction for non-itemizers comes off household resources + # Michigan Treasury FAQ: for 2020 the charitable cash contribution "is a + # deduction from adjusted gross income and therefore Total Household + # Resources as well". U.S. Form 1040 line 10b: min(500, 300). + period: 2020 + input: + people: + head: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MI + output: + charitable_contributions_ald: 300 + mi_household_resources: 39_700 + +- name: The 2021 charitable deduction for non-itemizers is not taken from household resources + # The 2021 deduction is taken from AGI, so it is not a deduction from + # total household resources (Michigan Treasury FAQ). + period: 2021 + input: + people: + head: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MI + output: + charitable_deduction_for_non_itemizers: 300 + mi_household_resources: 40_000 From 2914342905f4e7a0ee8c6555aa58e5e115591950 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 20:00:50 -0400 Subject: [PATCH 05/12] Test invariants of the 2020 above-the-line charitable deduction Seeded and Hypothesis households in seven states, 2020 and 2021: the deduction is min(cash gifts, Form 1040 line 10b cap) for non-itemizers and zero otherwise; AGI is wages less it; Schedule A totals are exactly their components; the simulation's income tax is that of the chosen branch and the lower of the two; California and New York add it back and Massachusetts Part B AGI keeps it. Co-Authored-By: Claude Opus 5.5 --- ...charitable_contributions_ald_invariants.py | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py diff --git a/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py b/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py new file mode 100644 index 00000000000..d64750a049b --- /dev/null +++ b/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py @@ -0,0 +1,196 @@ +"""Invariants for the 2020 above-the-line charitable deduction. + +26 U.S.C. 62(a)(22) (CARES Act sec. 2204) lets an individual who does not +itemize deduct up to $300 of cash charitable contributions in arriving at +adjusted gross income for taxable years beginning in 2020 only. The 2020 +Instructions for Form 1040, line 10b, cap it at $300 for a joint return and +$150 for married filing separately. Because adjusted gross income then depends +on the itemization choice, tax_unit_itemizes compares the deductions of the +branches that itemize and that do not. + +Invariants, for every household, written from the statute and the form rather +than read from the model's parameters: + +1. Bounds and eligibility: the deduction is min(cash gifts, cap) for a unit + that does not itemize and zero for one that does, and zero in 2021. +2. Adjusted gross income is gross income less the above-the-line deductions, + which include the charitable deduction (2020 Form 1040 lines 9, 10c, 11). +3. No gift is deducted twice: itemized deductions are exactly the Schedule A + components, in 2020 and 2021. +4. Consistency of the choice: the income tax of the simulation equals the tax + of the branch for the choice it makes, which is the lower of the two + (differential: main simulation against the branch that recomputes it). +5. Decoupled states add it back (California, New York) and Massachusetts + Part B income leaves it in (TIR 20-9, B(6)). +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 # dollars + +# 2020 Instructions for Form 1040, line 10b. +CAP_2020 = { + "SINGLE": 300, + "JOINT": 300, + "SEPARATE": 150, + "HEAD_OF_HOUSEHOLD": 300, + "SURVIVING_SPOUSE": 300, +} +SCHEDULE_A = [ + "charitable_deduction", + "interest_deduction", + "salt_deduction", + "medical_expense_deduction", + "casualty_loss_deduction", + "misc_deduction", +] +STATES = ["TX", "CA", "NY", "MA", "AZ", "MI", "KY"] + + +@st.composite +def tax_units(draw): + money = lambda high: st.one_of( # noqa: E731 + st.just(0.0), st.integers(1, high).map(float) + ) + return { + "filing": draw(st.sampled_from(["single", "joint", "separate", "hoh"])), + "state": draw(st.sampled_from(STATES)), + "wages": draw(money(250_000)), + "cash": draw(money(5_000)), + "noncash": draw(money(5_000)), + "mortgage": draw(money(30_000)), + "medical": draw(money(20_000)), + } + + +def _seeded_units(n=60, seed=20261012): + rng = np.random.default_rng(seed) + + def some(high, p=0.6): + return float(round(rng.uniform(1, high))) if rng.random() < p else 0.0 + + return [ + { + "filing": str(rng.choice(["single", "joint", "separate", "hoh"])), + "state": str(rng.choice(STATES)), + "wages": some(250_000, 0.9), + "cash": some(5_000, 0.8), + "noncash": some(5_000, 0.3), + "mortgage": some(30_000, 0.5), + "medical": some(20_000, 0.3), + } + for _ in range(n) + ] + + +def _situation(units, year): + people = {} + groups = {"tax_units": {}, "households": {}, "marital_units": {}} + for i, u in enumerate(units): + head = f"head_{i}" + people[head] = { + "age": {year: 45}, + "employment_income": {year: u["wages"]}, + "charitable_cash_donations": {year: u["cash"]}, + "charitable_non_cash_donations": {year: u["noncash"]}, + "deductible_mortgage_interest": {year: u["mortgage"]}, + "other_medical_expenses": {year: u["medical"]}, + } + members = [head] + if u["filing"] == "joint": + people[f"spouse_{i}"] = {"age": {year: 43}} + members.append(f"spouse_{i}") + if u["filing"] == "hoh": + people[f"child_{i}"] = {"age": {year: 8}} + members.append(f"child_{i}") + groups["marital_units"][f"couple_{i}"] = { + "members": [m for m in members if not m.startswith("child")] + } + if u["filing"] == "hoh": + groups["marital_units"][f"child_unit_{i}"] = {"members": [f"child_{i}"]} + tax_unit = {"members": members} + if u["filing"] == "separate": + tax_unit["filing_status"] = {year: "SEPARATE"} + groups["tax_units"][f"tax_unit_{i}"] = tax_unit + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: u["state"]}, + } + return {"people": people, **groups} + + +def _check(units, year): + sim = Simulation(situation=_situation(units, year)) + + def calc(variable): + return np.asarray(sim.calculate(variable, year), dtype=float) + + filing = sim.calculate("filing_status", year).decode_to_str() + state = np.array([u["state"] for u in units]) + cash = np.array([u["cash"] for u in units]) + itemizes = calc("tax_unit_itemizes").astype(bool) + ald = calc("charitable_contributions_ald") + + # 1. Bounds and eligibility. + if year == 2020: + cap = np.array([CAP_2020[f] for f in filing], dtype=float) + expected = np.where(itemizes, 0, np.minimum(cash, cap)) + else: + expected = np.zeros_like(cash) + np.testing.assert_allclose(ald, expected, atol=TOLERANCE) + + # 2. AGI: line 11 = line 9 - line 10c. Wages are the only income and the + # charitable deduction the only adjustment. + wages = np.array([u["wages"] for u in units]) + agi = calc("adjusted_gross_income") + np.testing.assert_allclose(calc("above_the_line_deductions"), ald, atol=TOLERANCE) + np.testing.assert_allclose(agi, wages - ald, atol=1) + + # 3. No gift deducted twice. + components = sum(calc(variable) for variable in SCHEDULE_A) + np.testing.assert_allclose( + calc("total_itemized_taxable_income_deductions"), components, atol=1 + ) + + # 4. The simulation's tax is the tax of the branch it chooses, the lower. + if_itemizing = calc("tax_liability_if_itemizing") + if_not = calc("tax_liability_if_not_itemizing") + income_tax = calc("income_tax") + np.testing.assert_allclose( + income_tax, np.where(itemizes, if_itemizing, if_not), atol=1 + ) + np.testing.assert_allclose(income_tax, np.minimum(if_itemizing, if_not), atol=1) + + # 5. Decoupled states. + in_state = lambda code: state == code # noqa: E731 + np.testing.assert_allclose( + calc("ca_additions")[in_state("CA")], ald[in_state("CA")] + ) + np.testing.assert_allclose( + calc("ny_additions")[in_state("NY")], ald[in_state("NY")] + ) + # Massachusetts does not adopt 62(a)(22) (TIR 20-9, B(6)): Part B AGI is + # the wages. + ma = in_state("MA") + np.testing.assert_allclose(calc("ma_part_b_agi")[ma], wages[ma], atol=1) + + +@pytest.mark.parametrize("year", [2020, 2021]) +def test_seeded_population(year): + _check(_seeded_units(), year) + + +@settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(tax_units(), min_size=5, max_size=20)) +def test_random_households_2020(units): + _check(units, 2020) From e8d0636a9d70c0e94d33b837e9e123eefb4ef5e5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:26:11 -0400 Subject: [PATCH 06/12] Apply the statute's $300 cap and Sol review r1 findings - 62(a)(22) caps the 2020 deduction at $300 for every return; the 2020 Form 1040 instructions' $150 for married filing separately has no basis in the statute, which governs (PolicyEngine statute-first rule, 2026-10-19). - Count only the head's and spouse's cash gifts to 170(b)(1)(A) organizations (resolved_charitable_cash_donations_50_pct); a dependent's gifts are on the dependent's own return. New charitable_contributions_ald_person gives each filer's part, by their own gifts, for per-person AGI; Kentucky adds back that part. - Without branching (as CI runs YAML tests), the 2020 itemization choice counts what not itemizing takes off AGI with the standard deduction. - The tuition and fees MAGI subtracts line 10b (2020 Pub. 970 Worksheet 6-1 line 2). - New York: ny_recomputed_federal_agi (IT-201 line 19a) adds back IT-558 A-003; NY AGI, the IT-196 floors and limitation, and the credits read it (2020 IT-196, IT-213, IT-214, IT-216, IT-221). - Minnesota: AMT income adds back M1NC line 6 (2020 M1MT line 4); the Social Security worksheet step 8 leaves out line 10b (2020 M1M p.8). - Michigan's 2020 MI-1040CR line 30 cite is file page 28. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-2020-charitable-ald.fixed.md | 2 +- .../irs/ald/charitable_contributions/cap.yaml | 9 +- .../household_resources_adjustments.yaml | 2 +- ...charitable_contributions_ald_invariants.py | 100 +++++++++++++----- .../charitable_contributions_ald.yaml | 25 +++-- .../charitable_contributions/integration.yaml | 96 +++++++++++++++-- .../states/ky/tax/income/ky_additions.yaml | 10 +- .../mn/tax/income/mn_amt_taxable_income.yaml | 24 +++++ .../mn_social_security_subtraction.yaml | 32 ++++++ .../adjusted_gross_income/ny_additions.yaml | 41 ------- .../ny_recomputed_federal_agi.yaml | 72 +++++++++++++ .../capped_qualified_tuition_expenses_ald.py | 6 ++ .../charitable_contributions_ald.py | 18 +++- .../charitable_contributions_ald_person.py | 26 +++++ .../deductions/tax_unit_itemizes.py | 16 ++- .../gov/states/ky/tax/income/ky_additions.py | 11 +- .../tax/income/amt/mn_amt_taxable_income.py | 8 +- .../mn_social_security_subtraction.py | 8 +- .../adjusted_gross_income/ny_additions.py | 11 +- .../income/adjusted_gross_income/ny_agi.py | 3 +- .../ny_disability_income_exclusion.py | 2 +- .../ny_recomputed_federal_agi.py | 26 +++++ .../credits/cdcc/ny_cdcc_federal_rate.py | 2 +- .../income/credits/ctc/ny_additional_ctc.py | 2 +- .../credits/ctc/ny_ctc_post_2024_phase_out.py | 2 +- .../tax/income/credits/ctc/ny_ctc_pre_2024.py | 2 +- .../credits/ny_household_credit_potential.py | 2 +- .../credits/ny_household_gross_income.py | 3 +- .../credits/ny_real_property_tax_credit.py | 2 +- .../ny_itemized_deductions_phase_out.py | 2 +- ...y_itemized_deductions_phase_out_applies.py | 2 +- .../pre_tcja/ny_casualty_loss_deduction.py | 3 +- .../pre_tcja/ny_medical_expense_deduction.py | 10 +- .../itemized/pre_tcja/ny_misc_deduction.py | 4 +- 34 files changed, 451 insertions(+), 133 deletions(-) delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald_person.py create mode 100644 policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py diff --git a/changelog.d/fix-2020-charitable-ald.fixed.md b/changelog.d/fix-2020-charitable-ald.fixed.md index 7b6db27f7f9..e809f6e210d 100644 --- a/changelog.d/fix-2020-charitable-ald.fixed.md +++ b/changelog.d/fix-2020-charitable-ald.fixed.md @@ -1 +1 @@ -Take the 2020 charitable deduction for non-itemizers (IRC 62(a)(22), Form 1040 line 10b) above the line, so it reduces adjusted gross income, and stop adding the 2020 and 2021 non-itemizer amount to itemized deductions. +Take the 2020 charitable deduction for non-itemizers (IRC 62(a)(22), Form 1040 line 10b) above the line, capped at $300 for every return as the statute reads, so it reduces adjusted gross income; stop adding the 2020 and 2021 non-itemizer amount to itemized deductions; and add it back in states that did not adopt it. diff --git a/policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml b/policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml index f1d3d640582..dc8e56a74f1 100644 --- a/policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml +++ b/policyengine_us/parameters/gov/irs/ald/charitable_contributions/cap.yaml @@ -17,9 +17,10 @@ metadata: # below-the-line 26 U.S.C. 170(p). - title: "Pub. L. 116-260, div. EE, sec. 212 (Consolidated Appropriations Act, 2021) #page 1888-1889" href: https://www.congress.gov/116/plaws/publ260/PLAW-116publ260.pdf#page=1888 - # Form 1040 line 10b: $300 if single, head of household or qualifying - # widow(er), $300 if married filing jointly, $150 if married filing - # separately. + # Form 1040 line 10b allows $300 a return. The instructions' $150 for + # married filing separately has no basis in 62(a)(22) or 62(f), which + # cap the deduction at $300 for every eligible individual, so the + # statute governs (PolicyEngine statute-first rule, 2026-10-19). - title: 2020 Instructions for Form 1040, line 10b href: https://www.irs.gov/pub/irs-prior/i1040gi--2020.pdf#page=30 @@ -33,7 +34,7 @@ JOINT: 2021-01-01: 0 SEPARATE: 2000-01-01: 0 - 2020-01-01: 150 + 2020-01-01: 300 2021-01-01: 0 HEAD_OF_HOUSEHOLD: 2000-01-01: 0 diff --git a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml index aff50d4db09..d0b0322d0ba 100644 --- a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml +++ b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_adjustments.yaml @@ -37,7 +37,7 @@ metadata: label: Michigan household resources adjustments reference: - title: Michigan 2020 Individual Income Tax Instructions, MI-1040CR line 30 - href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2020/Book_MI-1040_instructions_only.pdf#page=32 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2020/Book_MI-1040_instructions_only.pdf#page=28 - title: Michigan Treasury FAQ, charitable contribution deductions from AGI and total household resources (2020 and 2021) href: https://www.michigan.gov/taxes/questions/iit/accordion/covid19-cares/charitable-contribution-deduct556422 - title: Michigan 2024 Individual Income Tax Instructions, MI-1040CR line 30 diff --git a/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py b/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py index d64750a049b..2f597f04e40 100644 --- a/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py +++ b/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py @@ -2,17 +2,19 @@ 26 U.S.C. 62(a)(22) (CARES Act sec. 2204) lets an individual who does not itemize deduct up to $300 of cash charitable contributions in arriving at -adjusted gross income for taxable years beginning in 2020 only. The 2020 -Instructions for Form 1040, line 10b, cap it at $300 for a joint return and -$150 for married filing separately. Because adjusted gross income then depends -on the itemization choice, tax_unit_itemizes compares the deductions of the -branches that itemize and that do not. +adjusted gross income for taxable years beginning in 2020 only. The statute +caps it at $300 for every return; the 2020 Instructions for Form 1040, line +10b, also give $300 for a joint return, and their $150 for married filing +separately has no basis in the statute, which governs. Because adjusted gross +income then depends on the itemization choice, tax_unit_itemizes compares the +deductions of the branches that itemize and that do not. Invariants, for every household, written from the statute and the form rather than read from the model's parameters: -1. Bounds and eligibility: the deduction is min(cash gifts, cap) for a unit - that does not itemize and zero for one that does, and zero in 2021. +1. Bounds and eligibility: the deduction is min(the head's and spouse's cash + gifts, $300) for a unit that does not itemize and zero for one that does, + and zero in 2021. A dependent's gifts are on the dependent's own return. 2. Adjusted gross income is gross income less the above-the-line deductions, which include the charitable deduction (2020 Form 1040 lines 9, 10c, 11). 3. No gift is deducted twice: itemized deductions are exactly the Schedule A @@ -20,8 +22,12 @@ 4. Consistency of the choice: the income tax of the simulation equals the tax of the branch for the choice it makes, which is the lower of the two (differential: main simulation against the branch that recomputes it). -5. Decoupled states add it back (California, New York) and Massachusetts - Part B income leaves it in (TIR 20-9, B(6)). +5. Decoupled states add it back (California; New York's recomputed federal + AGI, IT-201 line 19a) and Massachusetts Part B income leaves it in (TIR + 20-9, B(6)). +6. Without branching (as CI runs YAML tests), the choice still leaves the + lower taxable income of the two branches: in 2020 the comparison counts + what not itemizing takes off adjusted gross income. """ import numpy as np @@ -29,18 +35,15 @@ from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st +from policyengine_core.periods import instant +from policyengine_core.reforms import Reform + from policyengine_us import Simulation TOLERANCE = 0.01 # dollars -# 2020 Instructions for Form 1040, line 10b. -CAP_2020 = { - "SINGLE": 300, - "JOINT": 300, - "SEPARATE": 150, - "HEAD_OF_HOUSEHOLD": 300, - "SURVIVING_SPOUSE": 300, -} +# 26 U.S.C. 62(a)(22): "not to exceed $300", for every return. +CAP_2020 = 300 SCHEDULE_A = [ "charitable_deduction", "interest_deduction", @@ -65,6 +68,7 @@ def tax_units(draw): "noncash": draw(money(5_000)), "mortgage": draw(money(30_000)), "medical": draw(money(20_000)), + "child_cash": draw(money(1_000)), } @@ -83,8 +87,24 @@ def some(high, p=0.6): "noncash": some(5_000, 0.3), "mortgage": some(30_000, 0.5), "medical": some(20_000, 0.3), + "child_cash": some(1_000, 0.3), } for _ in range(n) + ] + [ + # Single New Hampshire filers (no state or local tax deduction) whose + # Schedule A is the $12,400 standard deduction plus k, with $300 of + # gifts: itemizing pays only when k > 300. + { + "filing": "single", + "state": "NH", + "wages": 60_000.0, + "cash": 300.0, + "noncash": 0.0, + "mortgage": 12_400.0 - 300 + k, + "medical": 0.0, + "child_cash": 0.0, + } + for k in [-400, -200, -100, 0, 100, 299, 301, 400] ] @@ -106,7 +126,10 @@ def _situation(units, year): people[f"spouse_{i}"] = {"age": {year: 43}} members.append(f"spouse_{i}") if u["filing"] == "hoh": - people[f"child_{i}"] = {"age": {year: 8}} + people[f"child_{i}"] = { + "age": {year: 15}, + "charitable_cash_donations": {year: u["child_cash"]}, + } members.append(f"child_{i}") groups["marital_units"][f"couple_{i}"] = { "members": [m for m in members if not m.startswith("child")] @@ -124,7 +147,19 @@ def _situation(units, year): return {"people": people, **groups} -def _check(units, year): +def _no_branching(parameters): + parameters.gov.simulation.branch_to_determine_itemization.update( + start=instant("2000-01-01"), stop=instant("2039-12-31"), value=False + ) + return parameters + + +class NoBranching(Reform): + def apply(self): + self.modify_parameters(_no_branching) + + +def _check(units, year, without_branching=False): sim = Simulation(situation=_situation(units, year)) def calc(variable): @@ -138,8 +173,7 @@ def calc(variable): # 1. Bounds and eligibility. if year == 2020: - cap = np.array([CAP_2020[f] for f in filing], dtype=float) - expected = np.where(itemizes, 0, np.minimum(cash, cap)) + expected = np.where(itemizes, 0, np.minimum(cash, CAP_2020)) else: expected = np.zeros_like(cash) np.testing.assert_allclose(ald, expected, atol=TOLERANCE) @@ -171,18 +205,34 @@ def calc(variable): np.testing.assert_allclose( calc("ca_additions")[in_state("CA")], ald[in_state("CA")] ) + ny = in_state("NY") np.testing.assert_allclose( - calc("ny_additions")[in_state("NY")], ald[in_state("NY")] + calc("ny_recomputed_federal_agi")[ny], (agi + ald)[ny], atol=1 ) # Massachusetts does not adopt 62(a)(22) (TIR 20-9, B(6)): Part B AGI is # the wages. ma = in_state("MA") np.testing.assert_allclose(calc("ma_part_b_agi")[ma], wages[ma], atol=1) + # 6. Without branching, the choice leaves the lower taxable income. (In + # 2021 the comparison ignores the 170(p) deduction, a separate issue.) + if without_branching: + plain = Simulation(situation=_situation(units, year), reform=NoBranching) + taxable_income = np.asarray(plain.calculate("taxable_income", year)) + branch_incomes = [ + np.asarray(plain.branches[name].calculate("taxable_income", year)) + for name in ("itemizing", "not_itemizing") + ] + assert (taxable_income <= np.minimum(*branch_incomes) + 1).all() + + +def test_seeded_population_2020(): + # The seeded population includes households at the itemization margin. + _check(_seeded_units(), 2020, without_branching=True) + -@pytest.mark.parametrize("year", [2020, 2021]) -def test_seeded_population(year): - _check(_seeded_units(), year) +def test_seeded_population_2021(): + _check(_seeded_units(), 2021) @settings( diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml index efbe526e7df..84e1d6dfdaa 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.yaml @@ -1,7 +1,9 @@ -# 26 U.S.C. 62(a)(22) and 62(f) (CARES Act sec. 2204), 2020 only. Caps from -# the 2020 Instructions for Form 1040, line 10b: $300 if single, head of -# household or qualifying widow(er), $300 if married filing jointly, $150 if -# married filing separately. +# 26 U.S.C. 62(a)(22) and 62(f) (CARES Act sec. 2204), 2020 only: "the +# amount (not to exceed $300) of qualified charitable contributions made by +# an eligible individual". $300 for every return, as the 2020 Instructions +# for Form 1040, line 10b, give for single, head of household, qualifying +# widow(er) and joint filers. The instructions' $150 for married filing +# separately has no basis in the statute, which governs. - name: Case 1, 2020 non-itemizer below the cap deducts all cash gifts. period: 2020 @@ -39,14 +41,14 @@ output: charitable_contributions_ald: 300 -- name: Case 5, 2020 married filing separately is capped at $150. +- name: Case 5, 2020 married filing separately is capped at $300, as the statute reads. period: 2020 input: charitable_cash_donations: 400 filing_status: SEPARATE tax_unit_itemizes: false output: - charitable_contributions_ald: 150 + charitable_contributions_ald: 300 - name: Case 6, 2020 head of household is capped at $300. period: 2020 @@ -103,3 +105,14 @@ output: charitable_contributions_ald: 0 charitable_deduction_for_non_itemizers: 300 + +- name: Case 12, gifts to organizations that are not 170(b)(1)(A) organizations do not qualify (62(f)(2)(C)). + period: 2020 + input: + charitable_cash_donations: 500 + charitable_cash_donations_non_50_pct_orgs: 400 + filing_status: SINGLE + tax_unit_itemizes: false + output: + # 500 - 400 = 100 of cash to 170(b)(1)(A) organizations. + charitable_contributions_ald: 100 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml index 43f6f09e63d..1c1bbfb6651 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml @@ -1,5 +1,7 @@ # Worked from the 2020 Form 1040 (lines 9, 10a-10c, 11, 12 and 15) and its -# instructions: line 10b (file page 30), the 2020 Tax Rate Schedules (file +# instructions: line 10b (file page 30; the cap is $300 for every return, as +# 26 U.S.C. 62(a)(22) reads, not the instructions' $150 for married filing +# separately), the 2020 Tax Rate Schedules (file # page 111), the Standard Deduction Chart for People Born Before January 2, # 1956 (file page 33), the Social Security Benefits Worksheet (file page 31, # line 6 subtracts line 10b) and the Unemployment Compensation Exclusion @@ -69,7 +71,7 @@ # Schedule Y-1: 1,975 + 12% x (54,900 - 19,750). income_tax_main_rates: 6_193 -- name: Case 3, 2020 married filing separately deducts $150. +- name: Case 3, 2020 married filing separately deducts $300, as the statute reads. period: 2020 input: people: @@ -86,14 +88,16 @@ members: [person1] state_code: NH output: - charitable_contributions_ald: 150 - # Line 11: 50,000 - 150. - adjusted_gross_income: 49_850 + # 62(a)(22): not to exceed $300; the instructions' $150 has no basis in + # the statute. + charitable_contributions_ald: 300 + # Line 11: 50,000 - 300. + adjusted_gross_income: 49_700 standard_deduction: 12_400 - # Line 15: 49,850 - 12,400. - taxable_income: 37_450 - # Schedule Y-2: 987.50 + 12% x (37,450 - 9,875). - income_tax_main_rates: 4_296.5 + # Line 15: 49,700 - 12,400. + taxable_income: 37_300 + # Schedule Y-2: 987.50 + 12% x (37,300 - 9,875). + income_tax_main_rates: 4_278.5 - name: Case 4, 2020 itemizer deducts the gifts on Schedule A only, once. period: 2020 @@ -259,3 +263,77 @@ taxable_income_deductions: 12_850 # Line 15: 40,000 - 12,850. taxable_income: 27_150 + +- name: Case 10, 2020 dependent's own gifts are not on the parent's return. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + person2: + age: 16 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NH + output: + # 62(a)(22) counts contributions "made by an eligible individual"; the + # child's gifts belong on the child's own return. + charitable_contributions_ald: 0 + adjusted_gross_income: 40_000 + +- name: Case 11, 2020 line 10b lowers the income that limits the tuition and fees deduction. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 80_050 + charitable_cash_donations: 300 + qualified_tuition_expenses: 4_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + charitable_contributions_ald: 300 + # 2020 Pub. 970 Worksheet 6-1: line 1 = 80,050, line 2 includes line 10b + # (300), MAGI = 79,750, over $65,000 but not over $80,000 for a single + # filer, so the deduction is limited to $2,000 (Form 8917 line 5). + capped_qualified_tuition_expenses_ald: 2_000 + # Line 11: 80,050 - 300 - 2,000. + adjusted_gross_income: 77_750 + +- name: Case 12, 2020 choice without branching weighs line 10b with the standard deduction. + period: 2020 + input: + gov.simulation.branch_to_determine_itemization: false + people: + person1: + age: 40 + employment_income: 60_000 + deductible_mortgage_interest: 12_300 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + # Case 5 without branching (as CI runs YAML tests): Schedule A 12,600 is + # less than the 12,400 standard deduction plus the 300 that not + # itemizing takes off AGI. + tax_unit_itemizes: false + charitable_contributions_ald: 300 + adjusted_gross_income: 59_700 + taxable_income: 47_300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml index 179c0d003c4..8417e2f605c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml @@ -22,7 +22,7 @@ ky_additions: 300 ky_agi: 40_000 -- name: Case 2, 2020 joint federal non-itemizers add back half of the deduction each. +- name: Case 2, 2020 joint federal non-itemizers add back each spouse's own part of the deduction. period: 2020 input: people: @@ -41,9 +41,11 @@ members: [person1, person2] state_code: KY output: - # Federal person AGI deducts half of the $300 from each spouse. - adjusted_gross_income_person: [49_850, 29_850] - ky_additions: [150, 150] + # The $300 deduction is person1's: only person1 gave. Federal person AGI + # deducts it from person1 (charitable_contributions_ald_person). + charitable_contributions_ald_person: [300, 0] + adjusted_gross_income_person: [49_700, 30_000] + ky_additions: [300, 0] ky_agi: [50_000, 30_000] - name: Case 3, 2021 deduction for non-itemizers is below the line, so nothing is added back. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_amt_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_amt_taxable_income.yaml index 4310e3a2c90..0b1ca031180 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_amt_taxable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_amt_taxable_income.yaml @@ -45,3 +45,27 @@ state_code: MN output: mn_amt_taxable_income: 200_000 + +- name: MN AMT taxable income adds back the 2020 federal charitable deduction for non-itemizers + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MN + output: + # Federal AGI (M1MT line 1) = 40,000 - 300 (Form 1040 line 10b). Line 4 + # includes Schedule M1NC line 6, the 300 added back (2020 M1MT + # instructions, file page 2). No itemizing, no subtractions. + adjusted_gross_income: 39_700 + charitable_contributions_ald: 300 + mn_itemizing: false + mn_amt_taxable_income: 40_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_social_security_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_social_security_subtraction.yaml index 17cb6090594..e90fead9244 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_social_security_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_social_security_subtraction.yaml @@ -153,3 +153,35 @@ # AGI equals the $86,410 threshold, so the simplified subtraction is not # reduced and the full $20,000 of taxable benefits is subtracted. mn_social_security_subtraction: 20_000 + +- name: MN social security subtraction 2020 worksheet step 8 leaves out Form 1040 line 10b + absolute_error_margin: 0.01 + period: 2020 + input: + # 2020 Worksheet for line 39 constants (2020 M1M instructions, file + # page 8), as the parameters start in 2021. + gov.states.mn.tax.income.subtractions.social_security.income_amount.SINGLE: 62_090 + gov.states.mn.tax.income.subtractions.social_security.alternative_amount.SINGLE: 4_090 + people: + person1: + age: 67 + employment_income: 65_000 + social_security_retirement: 10_000 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MN + output: + charitable_contributions_ald: 300 + # Federal: provisional income 65,000 - 300 + 5,000 = 69,700, so 85% of + # benefits, 8,500, are taxable. + tax_unit_taxable_social_security: 8_500 + # Steps 1-3: 73,500 - 8,500 = 65,000; step 5: 5,000; step 7: 70,000; + # step 8: Schedule 1 lines 10-19 and 22 only, 0 (not line 10b); + # step 9: 70,000; step 11: 70,000 - 62,090 = 7,910; step 12: 1,582; + # step 14: 4,090 - 1,582 = 2,508; step 18: min(2,508, 8,500). + mn_social_security_subtraction: 2_508 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml deleted file mode 100644 index 68633227a96..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# 2020 Form IT-558 instructions, addition A-003: the IRC 62(a)(22) -# charitable deduction is added back to recompute federal AGI. -# https://www.tax.ny.gov/pdf/2020/inc/it558i_2020.pdf#page=2 - -- name: Case 1, 2020 federal non-itemizer adds back the line 10b deduction. - period: 2020 - input: - people: - person1: - age: 40 - employment_income: 40_000 - charitable_cash_donations: 500 - tax_units: - tax_unit: - members: [person1] - households: - household: - members: [person1] - state_code: NY - output: - adjusted_gross_income: 39_700 - ny_additions: 300 - ny_agi: 40_000 - -- name: Case 2, 2021 deduction for non-itemizers is below the line, so nothing is added back. - period: 2021 - input: - people: - person1: - age: 40 - employment_income: 40_000 - charitable_cash_donations: 500 - tax_units: - tax_unit: - members: [person1] - households: - household: - members: [person1] - state_code: NY - output: - ny_additions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.yaml new file mode 100644 index 00000000000..40355240a03 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.yaml @@ -0,0 +1,72 @@ +# 2020 Form IT-201 instructions, line 19a worksheet (file page 16): recomputed +# federal AGI is federal AGI plus the Form IT-558 addition adjustments. IT-558 +# addition A-003 (instructions, file page 2) adds back the IRC 62(a)(22) +# charitable deduction, which New York did not adopt for 2020. New York AGI +# starts from line 19a (IT-201 instructions, file page 17), and Form IT-196 +# lines 2 and 26 take the medical and miscellaneous floors from it. +# https://www.tax.ny.gov/pdf/2020/inc/it201i_2020.pdf#page=16 + +- name: Case 1, 2020 federal non-itemizer's line 10b deduction is added back on line 19a. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + output: + # Federal AGI (IT-201 line 19) 40,000 - 300; line 19a adds the 300 back. + adjusted_gross_income: 39_700 + ny_recomputed_federal_agi: 40_000 + ny_additions: 0 + ny_agi: 40_000 + +- name: Case 2, 2020 IT-196 floors use recomputed federal AGI. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 300 + other_medical_expenses: 6_000 + tax_units: + tax_unit: + members: [person1] + total_misc_deductions: 10_000 + households: + household: + members: [person1] + state_code: NY + output: + ny_recomputed_federal_agi: 40_000 + # IT-196 line 3: 10% of line 19a = 4,000; 6,000 - 4,000. + ny_medical_expense_deduction: 2_000 + # IT-196 line 26-27: 2% of line 19a = 800; 10,000 - 800. + ny_misc_deduction: 9_200 + +- name: Case 3, 2021 recomputed federal AGI is federal AGI. + period: 2021 + input: + people: + person1: + age: 40 + employment_income: 40_000 + charitable_cash_donations: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + output: + adjusted_gross_income: 40_000 + ny_recomputed_federal_agi: 40_000 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/capped_qualified_tuition_expenses_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/capped_qualified_tuition_expenses_ald.py index c634698f7e5..ac0389d9366 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/capped_qualified_tuition_expenses_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/capped_qualified_tuition_expenses_ald.py @@ -15,6 +15,8 @@ class capped_qualified_tuition_expenses_ald(Variable): ) reference = ( "https://www.irs.gov/pub/irs-pdf/f8917.pdf#page=2", + # 2020 Pub. 970, Worksheet 6-1, MAGI for the Tuition and Fees Deduction. + "https://www.irs.gov/pub/irs-prior/p970--2020.pdf#page=46", # Law was repealed starting tax year 2021. "https://irc.bloombergtax.com/public/uscode/doc/irc/section_222", ) @@ -53,6 +55,10 @@ def formula(tax_unit, period, parameters): for source in safe_sources: magi += not_dependent * max_(0, add(person, period, [source])) magi = tax_unit.sum(magi) + # 2020 Pub. 970 Worksheet 6-1 line 2 subtracts Form 1040 line 10b, + # the charitable deduction for non-itemizers, with the Schedule 1 + # adjustments (which this simplified MAGI leaves out). + magi = magi - tax_unit("charitable_contributions_ald", period) p = parameters(period).gov.irs.deductions.tuition_and_fees joint = filing_status == filing_status.possible_values.JOINT cap = where(joint, p.joint.calc(magi), p.non_joint.calc(magi)) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py index c94095e9e11..dc24903def4 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald.py @@ -12,10 +12,14 @@ class charitable_contributions_ald(Variable): "10b), which reduces adjusted gross income. It applies to taxable " "years beginning in 2020 only; the 2021 and later deductions for " "non-itemizers (26 U.S.C. 170(p)) are taken from adjusted gross " - "income in charitable_deduction_for_non_itemizers. Cash donations " - "are not split by recipient, so contributions to supporting " - "organizations and donor advised funds, which 62(f)(2) excludes, " - "are counted." + "income in charitable_deduction_for_non_itemizers. It counts the " + "head's and spouse's cash gifts to organizations described in " + "section 170(b)(1)(A); a dependent's gifts are on the dependent's " + "own return. The inputs do not separate gifts to supporting " + "organizations and donor advised funds, which 62(f)(2) excludes, so " + "they are counted. The $300 cap applies to every return, including " + "married filing separately: the 2020 Form 1040 instructions' $150 " + "for separate returns has no basis in the statute." ) definition_period = YEAR reference = ( @@ -39,6 +43,10 @@ def formula(tax_unit, period, parameters): # 62(f)(1): an eligible individual is one who does not elect to # itemize deductions. itemizes = tax_unit("tax_unit_itemizes", period) - cash_donations = add(tax_unit, period, ["charitable_cash_donations"]) + # 62(f)(2): contributions made in cash to an organization described + # in section 170(b)(1)(A), by the head or spouse. + cash_donations = tax_unit_non_dep_add( + tax_unit, period, ["resolved_charitable_cash_donations_50_pct"] + ) filing_status = tax_unit("filing_status", period) return ~itemizes * min_(cash_donations, p.cap[filing_status]) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald_person.py new file mode 100644 index 00000000000..a925a24f78a --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/charitable_contributions_ald_person.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class charitable_contributions_ald_person(Variable): + value_type = float + entity = Person + label = "Above-the-line charitable deduction for non-itemizers for each person" + unit = USD + documentation = ( + "Each head's or spouse's part of the tax unit's 2020 above-the-line " + "charitable deduction (charitable_contributions_ald), in proportion " + "to their own cash gifts to organizations described in section " + "170(b)(1)(A), or equally when neither gave. 62(a)(22) counts the " + "contributions made by the eligible individual. A tax unit " + "dependent's part is zero." + ) + definition_period = YEAR + reference = ( + # CARES Act sec. 2204: 26 U.S.C. 62(a)(22) and 62(f). + "https://www.congress.gov/116/plaws/publ136/PLAW-116publ136.pdf#page=65", + ) + + def formula(person, period, parameters): + amount = person.tax_unit("charitable_contributions_ald", period) + own = person("resolved_charitable_cash_donations_50_pct", period) + return amount * filer_share(person, period, own) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py index 6909b31896e..188c2b67e87 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py @@ -31,11 +31,21 @@ def formula(tax_unit, period, parameters): # ceiling) and any standard deduction that phases out with it. # Each comes from the branch that makes that choice (the ones # that also give the tax liabilities compared below). - itemized_deductions = choice_branch(tax_unit, period, True).calculate( + itemizing = choice_branch(tax_unit, period, True) + not_itemizing = choice_branch(tax_unit, period, False) + itemized_deductions = itemizing.calculate( "itemized_taxable_income_deductions", period ) - standard_deduction = choice_branch(tax_unit, period, False).calculate( - "standard_deduction", period + # Not itemizing also lowers adjusted gross income (line 10b, and + # what follows from it, such as taxable Social Security), so the + # deductions compared are everything each choice takes from + # gross income: the standard deduction counts with that + # reduction. + agi_reduction = itemizing.calculate( + "adjusted_gross_income", period + ) - not_itemizing.calculate("adjusted_gross_income", period) + standard_deduction = ( + not_itemizing.calculate("standard_deduction", period) + agi_reduction ) elif limitation.applies and limitation.obbb.applies: standard_deduction = tax_unit("standard_deduction", period) diff --git a/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py b/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py index e5f3b3256a2..69320c38244 100644 --- a/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py +++ b/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py @@ -21,11 +21,6 @@ class ky_additions(Variable): def formula(person, period, parameters): # The 2020 federal charitable deduction for non-itemizers is added - # back in the share of it that adjusted_gross_income_person (and so - # ky_federal_agi) deducts from each filer: half each on a joint - # return. - filing_status = person.tax_unit("filing_status", period) - share = person("is_tax_unit_head_or_spouse", period) * where( - filing_status == filing_status.possible_values.JOINT, 0.5, 1.0 - ) - return share * person.tax_unit("charitable_contributions_ald", period) + # back in the part of it that adjusted_gross_income_person (and so + # ky_federal_agi) deducts from each filer. + return person("charitable_contributions_ald_person", period) diff --git a/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py b/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py index 83736e79da5..98ed4fdb4e1 100644 --- a/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py +++ b/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py @@ -12,11 +12,17 @@ class mn_amt_taxable_income(Variable): "https://www.revenue.state.mn.us/sites/default/files/2023-01/m1mt_22.pdf", # 2024 Schedule M1MT, line 12 "https://www.revenue.state.mn.us/sites/default/files/2025-07/m1mt-24.pdf#page=1", + # 2020 Schedule M1MT instructions, line 4: includes Schedule M1NC + # line 6, the federal charitable deduction for non-itemizers. + "https://www.revenue.state.mn.us/sites/default/files/2023-02/m1mt_20.pdf#page=2", ) defined_for = StateCode.MN def formula(tax_unit, period, parameters): fagi = tax_unit("adjusted_gross_income", period) + # Line 4 adds back the 2020 federal charitable deduction for + # non-itemizers (Schedule M1NC line 6). + charitable_addition = tax_unit("charitable_contributions_ald", period) itemizing = tax_unit("mn_itemizing", period) SOME_DEDUCTIONS = [ "charitable_deduction", @@ -32,4 +38,4 @@ def formula(tax_unit, period, parameters): "mn_social_security_subtraction", ] amt_subs = add(tax_unit, period, AMT_SUBTRACTIONS) - return fagi - some_itm_deds - amt_subs + return fagi + charitable_addition - some_itm_deds - amt_subs diff --git a/policyengine_us/variables/gov/states/mn/tax/income/subtractions/mn_social_security_subtraction.py b/policyengine_us/variables/gov/states/mn/tax/income/subtractions/mn_social_security_subtraction.py index 74d3c37ea4f..e6739183eee 100644 --- a/policyengine_us/variables/gov/states/mn/tax/income/subtractions/mn_social_security_subtraction.py +++ b/policyengine_us/variables/gov/states/mn/tax/income/subtractions/mn_social_security_subtraction.py @@ -10,6 +10,9 @@ class mn_social_security_subtraction(Variable): reference = ( "https://www.taxformfinder.org/forms/2021/2021-minnesota-form-m1m.pdf", "https://www.revenue.state.mn.us/sites/default/files/2024-01/m1m-22.pdf", + # 2020 Schedule M1M instructions, line 12 worksheet, step 8: federal + # Schedule 1 adjustments only, not Form 1040 line 10b. + "https://www.revenue.state.mn.us/sites/default/files/2024-01/m1m_20.pdf#page=8", ) defined_for = StateCode.MN @@ -47,7 +50,10 @@ def formula(tax_unit, period, parameters): sum_income = adj_income + oasdi_amount + tax_exempt_int us_ald = tax_unit("above_the_line_deductions", period) student_loan_int = add(tax_unit, period, ["student_loan_interest"]) - mn_ald = max_(0, us_ald - student_loan_int) + # Step 8 counts the Schedule 1 adjustments, so not the 2020 + # charitable deduction for non-itemizers (Form 1040 line 10b). + charitable_ald = tax_unit("charitable_contributions_ald", period) + mn_ald = max_(0, us_ald - student_loan_int - charitable_ald) income = max_(0, sum_income - mn_ald) net_income = max_(0, income - income_amount) alt_sub_amt = max_(0, alt_amount - (net_income * p.net_income_fraction)) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py index d945050c0a9..5186524a5fd 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_additions.py @@ -8,13 +8,10 @@ class ny_additions(Variable): unit = USD documentation = "Additions to NY AGI over federal AGI." definition_period = YEAR - reference = ( - "https://www.law.cornell.edu/regulations/new-york/20-NYCRR-112.2", - # 2020 Form IT-558 instructions, addition A-003: the IRC 62(a)(22) - # charitable deduction, which recomputes federal AGI (IT-201 line - # 19a). - "https://www.tax.ny.gov/pdf/2020/inc/it558i_2020.pdf#page=2", + reference = dict( + title="N.Y. Comp. Codes R. & Regs. tit. 20 § 112.2", + href="https://www.law.cornell.edu/regulations/new-york/20-NYCRR-112.2", ) defined_for = StateCode.NY - adds = ["ny_other_additions", "charitable_contributions_ald"] + adds = ["ny_other_additions"] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_agi.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_agi.py index e8ca05d62ae..ef552b070d2 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_agi.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_agi.py @@ -10,5 +10,6 @@ class ny_agi(Variable): reference = "https://www.law.cornell.edu/regulations/new-york/20-NYCRR-132.1" defined_for = StateCode.NY - adds = ["adjusted_gross_income", "ny_additions"] + # IT-201 line 19a, recomputed federal AGI, plus the New York additions. + adds = ["ny_recomputed_federal_agi", "ny_additions"] subtracts = ["ny_agi_subtractions"] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_disability_income_exclusion.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_disability_income_exclusion.py index fbad7a5fead..ee398ae9f00 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_disability_income_exclusion.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_disability_income_exclusion.py @@ -40,7 +40,7 @@ def formula(person, period, parameters): # Lines 1-10: each person's pay at most $100 a week, reduced dollar # for dollar by federal AGI above $15,000. excludable = tax_unit.sum(min_(disability_pay, p.cap)) - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) exclusion = max_(0, excludable - max_(0, agi - p.reduction_threshold)) # Line 11: spouses share it in proportion to their disability pay. total_pay = tax_unit.sum(disability_pay) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py new file mode 100644 index 00000000000..cfb3102a5b0 --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class ny_recomputed_federal_agi(Variable): + value_type = float + entity = TaxUnit + label = "New York recomputed federal adjusted gross income" + unit = USD + documentation = ( + "Federal adjusted gross income as reported, plus the Form IT-558 " + "addition adjustments and less its subtraction adjustments (Form " + "IT-201 line 19a). New York AGI, the credits and the itemized " + "deduction floors and limitation start from it. The adjustment " + "modeled is A-003: the 2020 federal charitable deduction for " + "non-itemizers (IRC 62(a)(22)), which New York did not adopt." + ) + definition_period = YEAR + reference = ( + # 2020 IT-201 instructions, line 19a worksheet. + "https://www.tax.ny.gov/pdf/2020/inc/it201i_2020.pdf#page=16", + # 2020 IT-558 instructions, addition A-003. + "https://www.tax.ny.gov/pdf/2020/inc/it558i_2020.pdf#page=2", + ) + defined_for = StateCode.NY + + adds = ["adjusted_gross_income", "charitable_contributions_ald"] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/cdcc/ny_cdcc_federal_rate.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/cdcc/ny_cdcc_federal_rate.py index 8b109c5074e..d230a05aac8 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/cdcc/ny_cdcc_federal_rate.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/cdcc/ny_cdcc_federal_rate.py @@ -20,6 +20,6 @@ def formula(tax_unit, period, parameters): # AGI for all years. New York does not use the federal Form 2441 # rate, so the 2021 ARPA rates do not apply. p = parameters(period).gov.states.ny.tax.income.credits.cdcc.federal_rate - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) steps = np.ceil(max_(0, agi - p.start) / p.increment) return max_(p.min, p.max - steps * p.step) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_additional_ctc.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_additional_ctc.py index 3055b122085..9e3a1cd425e 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_additional_ctc.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_additional_ctc.py @@ -11,7 +11,7 @@ class ny_additional_ctc(Variable): defined_for = StateCode.NY def formula(tax_unit, period, parameters): - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) p = parameters(period).gov.states.ny.tax.income.credits.ctc.additional match = p.amount.calc(agi) base_ctc = tax_unit("ny_ctc", period) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_post_2024_phase_out.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_post_2024_phase_out.py index a77307116da..bd3bbbb2040 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_post_2024_phase_out.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_post_2024_phase_out.py @@ -13,7 +13,7 @@ class ny_ctc_post_2024_phase_out(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ny.tax.income.credits.ctc - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) filing_status = tax_unit("filing_status", period) base_credit = tax_unit("ny_ctc_post_2024_base", period) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py index 24c4fd4914c..31f14cdbc20 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py @@ -21,7 +21,7 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ny.tax.income.credits.ctc person = tax_unit.members age = person("age", period) - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) # Original CTC rules (pre-2025 and post-2027) if not p.pre_tcja: diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_credit_potential.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_credit_potential.py index b71524d37c8..d6e79046274 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_credit_potential.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_credit_potential.py @@ -18,7 +18,7 @@ def formula(tax_unit, period, parameters): agi = add( tax_unit, period, - ["adjusted_gross_income", "spouse_separate_adjusted_gross_income"], + ["ny_recomputed_federal_agi", "spouse_separate_adjusted_gross_income"], ) # Single filers: based only on AGI. single = filing_status == filing_statuses.SINGLE diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_gross_income.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_gross_income.py index 9137289a93e..6bae9a74f09 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_gross_income.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_household_gross_income.py @@ -28,7 +28,8 @@ def formula(tax_unit, period, parameters): # Form IT-214, Step 3 "Determine household gross income" (line 16 = # sum of lines 9 through 15). Line 9 is federal AGI; the remaining # lines add income that is not already in AGI, "even if not taxable." - agi = tax_unit("adjusted_gross_income", period) # line 9 + # Line 9: recomputed federal AGI (IT-201 line 19a; IT-214 instructions). + agi = tax_unit("ny_recomputed_federal_agi", period) person = tax_unit.members # Line 11: Social Security payments not included on line 9 (i.e. the # portion of benefits not taxed federally). Line 11 instructions: diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_real_property_tax_credit.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_real_property_tax_credit.py index ed140e803bf..43a138072ee 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_real_property_tax_credit.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_real_property_tax_credit.py @@ -42,7 +42,7 @@ def formula(tax_unit, period, parameters): if rptc.uses_household_gross_income: income = tax_unit("ny_household_gross_income", period) else: - income = tax_unit("adjusted_gross_income", period) + income = tax_unit("ny_recomputed_federal_agi", period) # IT-214: "Your federal adjusted gross income cannot be an amount less # than zero. If the amount is less than zero, enter 0." income = max_(income, 0) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py index 9e160b62047..db906ce1a2f 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py @@ -79,7 +79,7 @@ def formula(tax_unit, period, parameters): # The defined_for guard already restricts this to filers whose federal # AGI exceeds the applicable amount (worksheet line 7), so the excess is # positive; max_ guards the vectorized boundary. - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) filing_status = tax_unit("filing_status", period) applicable_amount = p.start[filing_status] agi_excess = max_(agi - applicable_amount, 0) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out_applies.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out_applies.py index 414bda3c7e6..237ca558263 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out_applies.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out_applies.py @@ -19,7 +19,7 @@ def formula(tax_unit, period, parameters): # the pre-TCJA federal 26 U.S.C. § 68(b) applicable amount, which NY # continues to inflation-adjust. p = parameters(period).gov.states.ny.tax.income.deductions.itemized.phase_out - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) filing_status = tax_unit("filing_status", period) applicable_amount = p.start[filing_status] return agi > applicable_amount diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_casualty_loss_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_casualty_loss_deduction.py index b179c6f0fab..21af7553b5f 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_casualty_loss_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_casualty_loss_deduction.py @@ -24,6 +24,7 @@ def formula(tax_unit, period, parameters): # (not limited to federally declared disasters like federal post-2017) loss = add(tax_unit, period, ["casualty_loss"]) p = parameters(period).gov.irs.deductions.itemized.casualty - positive_agi = tax_unit("positive_agi", period) + # IT-196 line 17: 10% of recomputed federal AGI (IT-201 line 19a). + positive_agi = max_(0, tax_unit("ny_recomputed_federal_agi", period)) # Apply the 10% AGI floor (pre-TCJA rule) return max_(0, loss - positive_agi * p.floor) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_medical_expense_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_medical_expense_deduction.py index eb74491e9f4..c465812b652 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_medical_expense_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_medical_expense_deduction.py @@ -18,9 +18,9 @@ class ny_medical_expense_deduction(Variable): medical and dental expenses only above 10% of federal AGI; NY does not follow the federal 7.5% floor in place since 2017 (Public Law 115-97 § 11027 for 2017-2018, later law after that). Form IT-196 lines 1-4 apply - this floor to federal AGI (Form IT-201 or IT-203 line 19; line 19a, - recomputed federal AGI, in 2020-2022, which PolicyEngine does not - distinguish from federal AGI). Through 2017, NY took the federal + this floor to recomputed federal AGI (Form IT-201 or IT-203 line 19a, + ny_recomputed_federal_agi), which differs from federal AGI in 2020 by the + federal charitable deduction for non-itemizers. Through 2017, NY took the federal Schedule A amount, so the floor parameter follows the federal floor then. """ @@ -29,5 +29,7 @@ def formula(tax_unit, period, parameters): # above 10% of AGI, not the federal 7.5% floor in place since 2017 p = parameters(period).gov.states.ny.tax.income.deductions.itemized.medical expenses = tax_unit("itemized_medical_expenses", period) - medical_floor = p.floor * tax_unit("positive_agi", period) + # IT-196 line 2: recomputed federal AGI (IT-201 line 19a). + agi = max_(0, tax_unit("ny_recomputed_federal_agi", period)) + medical_floor = p.floor * agi return max_(0, expenses - medical_floor) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_misc_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_misc_deduction.py index b365a3da8de..8ac01079e92 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_misc_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/pre_tcja/ny_misc_deduction.py @@ -24,5 +24,7 @@ def formula(tax_unit, period, parameters): # with the 2% AGI floor (not suspended like federal post-2017) p = parameters(period).gov.irs.deductions.itemized.misc expenses = tax_unit("total_misc_deductions", period) - misc_floor = p.floor * tax_unit("positive_agi", period) + # IT-196 line 26: recomputed federal AGI (IT-201 line 19a). + agi = max_(0, tax_unit("ny_recomputed_federal_agi", period)) + misc_floor = p.floor * agi return max_(0, expenses - misc_floor) From 46d7f76301b0dcbf25024ef3c11d8e4d76010cbe Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:30:44 -0400 Subject: [PATCH 07/12] Share one itemization branch helper with the tax liability branches get_itemization_branch (tools/general.py) gives the itemizing and not_itemizing branches to tax_unit_itemizes and both tax liability variables, so every caller asks for the same inputs and shares one branch. PolicyEngine/policyengine-us#10038 adds the same helper with a claim of right override; whichever lands second keeps that version. Co-Authored-By: Claude Opus 5.5 --- policyengine_us/tools/general.py | 16 ++++++++++++++ .../deductions/tax_liability_if_itemizing.py | 10 ++------- .../tax_liability_if_not_itemizing.py | 10 ++------- .../deductions/tax_unit_itemizes.py | 21 +++++-------------- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index d2bd4f5fd5f..2f8254e327c 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -185,6 +185,22 @@ def filer_share(person, period, amounts): return where(total > 0, proportional, equal) +def get_itemization_branch(tax_unit, period, itemizes: bool): + """ + The branch of ``tax_unit``'s simulation that fixes ``tax_unit_itemizes``. + + It is named ``itemizing`` or ``not_itemizing``. Every caller asks for the + same inputs, so they share one branch: the choice itself and the tax + liabilities it compares. + """ + return get_override_branch( + tax_unit.simulation, + "itemizing" if itemizes else "not_itemizing", + period, + {"tax_unit_itemizes": np.full(tax_unit.count, itemizes, dtype=bool)}, + ) + + def sum_contained_tax_units(var, population, period): tax_unit = population.members.tax_unit.reference_entity values = tax_unit(var, period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py index bbfb5c097ba..532a205d3a4 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py @@ -9,11 +9,5 @@ class tax_liability_if_itemizing(Variable): definition_period = YEAR def formula(tax_unit, period, parameters): - simulation = tax_unit.simulation - itemized_branch = get_override_branch( - simulation, - "itemizing", - period, - {"tax_unit_itemizes": np.ones((tax_unit.count,), dtype=bool)}, - ) - return itemized_branch.calculate("income_tax", period) + branch = get_itemization_branch(tax_unit, period, itemizes=True) + return branch.calculate("income_tax", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py index 07279c5639b..1853e36cfd4 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py @@ -10,11 +10,5 @@ class tax_liability_if_not_itemizing(Variable): definition_period = YEAR def formula(tax_unit, period, parameters): - simulation = tax_unit.simulation - non_itemized_branch = get_override_branch( - simulation, - "not_itemizing", - period, - {"tax_unit_itemizes": np.zeros((tax_unit.count,), dtype=bool)}, - ) - return non_itemized_branch.calculate("income_tax", period) + branch = get_itemization_branch(tax_unit, period, itemizes=False) + return branch.calculate("income_tax", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py index 188c2b67e87..78ae5ec6e77 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py @@ -1,17 +1,6 @@ from policyengine_us.model_api import * -def choice_branch(tax_unit, period, itemizes): - """The branch of the simulation in which every tax unit itemizes, or - every tax unit does not.""" - return get_override_branch( - tax_unit.simulation, - "itemizing" if itemizes else "not_itemizing", - period, - {"tax_unit_itemizes": np.full(tax_unit.count, itemizes, dtype=bool)}, - ) - - class tax_unit_itemizes(Variable): value_type = bool entity = TaxUnit @@ -31,8 +20,8 @@ def formula(tax_unit, period, parameters): # ceiling) and any standard deduction that phases out with it. # Each comes from the branch that makes that choice (the ones # that also give the tax liabilities compared below). - itemizing = choice_branch(tax_unit, period, True) - not_itemizing = choice_branch(tax_unit, period, False) + itemizing = get_itemization_branch(tax_unit, period, itemizes=True) + not_itemizing = get_itemization_branch(tax_unit, period, itemizes=False) itemized_deductions = itemizing.calculate( "itemized_taxable_income_deductions", period ) @@ -56,9 +45,9 @@ def formula(tax_unit, period, parameters): # (the one that also gives tax_liability_if_itemizing). # Calculated before branching, so the branch inherits it. tax_unit("total_itemized_taxable_income_deductions", period) - itemized_deductions = choice_branch(tax_unit, period, True).calculate( - "itemized_taxable_income_deductions", period - ) + itemized_deductions = get_itemization_branch( + tax_unit, period, itemizes=True + ).calculate("itemized_taxable_income_deductions", period) else: standard_deduction = tax_unit("standard_deduction", period) itemized_deductions = tax_unit("itemized_taxable_income_deductions", period) From e4d51eac0219ba20c8e3b5fec710330a7eabeef8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 00:08:51 -0400 Subject: [PATCH 08/12] Keep the 2020 charitable deduction out of CVRP income; cite MN Laws 2023 - CVRP counts income before adjustments (Implementation Manual), so line 10b does not lower it. - Kentucky adds back each filer's part in ky_agi; ky_additions stays a pure input, so supplying it no longer drops the addback. - Minnesota: Laws 2023, ch. 1, sec. 22(c)(2) and (d)(2), retroactive for 2020, make the 62(a)(22) amount an addition and part of AMT income. Co-Authored-By: Claude Opus 5.5 --- .../mn/tax/income/additions/sources.yaml | 5 ++++- .../is_ca_cvrp_normal_rebate_eligible.yaml | 22 +++++++++++++++++++ .../income/{ky_additions.yaml => ky_agi.yaml} | 8 +++---- .../cvrp/is_ca_cvrp_normal_rebate_eligible.py | 16 +++++++++++--- .../gov/states/ky/tax/income/ky_additions.py | 9 -------- .../gov/states/ky/tax/income/ky_agi.py | 8 ++++++- .../tax/income/amt/mn_amt_taxable_income.py | 3 +++ 7 files changed, 53 insertions(+), 18 deletions(-) rename policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/{ky_additions.yaml => ky_agi.yaml} (90%) diff --git a/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml b/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml index e1660f48708..71830d9cb6d 100644 --- a/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml +++ b/policyengine_us/parameters/gov/states/mn/tax/income/additions/sources.yaml @@ -1,7 +1,8 @@ description: Minnesota adds these variables to federal adjusted gross income when computing its taxable income. values: # 2020 Schedule M1NC line 6 adds back the federal charitable deduction for - # non-itemizers (CARES Act sec. 2204), through Form M1M line 16. + # non-itemizers (CARES Act sec. 2204), through Form M1M line 16: Laws 2023, + # ch. 1, sec. 22(c)(2), effective retroactively for 2020. 2020-01-01: - form_4972_lumpsum_distributions - charitable_contributions_ald @@ -15,6 +16,8 @@ metadata: reference: - title: 2020 Minnesota Schedule M1NC, Federal Adjustments href: https://www.revenue.state.mn.us/sites/default/files/2024-02/m1nc_20.pdf#page=1 + - title: Laws of Minnesota 2023, chapter 1, section 22 (temporary additions and subtractions) + href: https://www.revisor.mn.gov/laws/2023/0/Session+Law/Chapter/1/ - title: 2021 Minnesota Schedule M1M, Income Additions and Subtractions, Page 3 href: https://www.taxformfinder.org/forms/2021/2021-minnesota-form-m1m.pdf#page=3 - title: 2022 Minnesota Schedule M1M, Income Additions and Subtractions, Page 3 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.yaml index a544bb87454..562db883780 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.yaml @@ -13,3 +13,25 @@ adjusted_gross_income: 150_000 output: is_ca_cvrp_normal_rebate_eligible: true + +- name: The 2020 charitable deduction for non-itemizers does not lower CVRP income. + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 150_200 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: CA + output: + # Form 1040 line 11 is 150,200 - 300 = 149,900, but the CVRP manual + # counts income before adjustments (Form 1040 lines 1-7, Schedule 1 + # lines 1-8): 150,200, over the $150,000 single cap. + adjusted_gross_income: 149_900 + is_ca_cvrp_normal_rebate_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_agi.yaml similarity index 90% rename from policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml rename to policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_agi.yaml index 8417e2f605c..e2b8e09398d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_additions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_agi.yaml @@ -1,5 +1,5 @@ # 2020 Form 740 instructions, Schedule M line 5: contributions claimed on -# federal Form 1040 line 10b are added back. +# federal Form 1040 line 10b are added back (in ky_agi, by each filer's part). # https://revenue.ky.gov/Forms/740%20Instructions%20Packet%205.7.21.pdf#page=27 - name: Case 1, 2020 single federal non-itemizer adds back the line 10b deduction. @@ -19,7 +19,7 @@ state_code: KY output: adjusted_gross_income_person: 39_700 - ky_additions: 300 + charitable_contributions_ald_person: 300 ky_agi: 40_000 - name: Case 2, 2020 joint federal non-itemizers add back each spouse's own part of the deduction. @@ -45,7 +45,6 @@ # deducts it from person1 (charitable_contributions_ald_person). charitable_contributions_ald_person: [300, 0] adjusted_gross_income_person: [49_700, 30_000] - ky_additions: [300, 0] ky_agi: [50_000, 30_000] - name: Case 3, 2021 deduction for non-itemizers is below the line, so nothing is added back. @@ -64,4 +63,5 @@ members: [person1] state_code: KY output: - ky_additions: 0 + charitable_contributions_ald_person: 0 + ky_agi: 40_000 diff --git a/policyengine_us/variables/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.py b/policyengine_us/variables/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.py index e3be7b19789..ba63fa86196 100644 --- a/policyengine_us/variables/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.py +++ b/policyengine_us/variables/gov/states/ca/calepa/carb/cvrp/is_ca_cvrp_normal_rebate_eligible.py @@ -9,12 +9,22 @@ class is_ca_cvrp_normal_rebate_eligible(Variable): documentation = ( "Eligible for California Clean Vehicle Rebate Project (CVRP) normal rebate" ) - reference = "https://cleanvehiclerebate.org/en/eligibility-guidelines" + reference = ( + "https://cleanvehiclerebate.org/en/eligibility-guidelines", + # CVRP Implementation Manual: gross annual income is Form 1040 lines + # 1-7 and Schedule 1 lines 1-8, before adjustments. + "http://web.archive.org/web/20220305190415/https://cleanvehiclerebate.org/sites/default/files/docs/nav/transportation/cvrp/documents/CVRP-Implementation-Manual.pdf#page=13", + ) defined_for = StateCode.CA def formula(person, period, parameters): - # AGI must be less than the threshold. - agi = person.tax_unit("adjusted_gross_income", period) + # AGI must be less than the threshold. The program counts income + # before adjustments, so the 2020 charitable deduction for + # non-itemizers (Form 1040 line 10b) does not lower it. + tax_unit = person.tax_unit + agi = tax_unit("adjusted_gross_income", period) + tax_unit( + "charitable_contributions_ald", period + ) filing_status = person.tax_unit("filing_status", period) caps = parameters(period).gov.states.ca.calepa.carb.cvrp.income_cap return agi <= caps[filing_status] diff --git a/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py b/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py index 69320c38244..94360c4773c 100644 --- a/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py +++ b/policyengine_us/variables/gov/states/ky/tax/income/ky_additions.py @@ -13,14 +13,5 @@ class ky_additions(Variable): "https://revenue.ky.gov/Forms/740%20Packet%20Instructions%205-9-23.pdf#page=23", "https://revenue.ky.gov/Forms/Schedule%20M-2021.pdf#page=1", "https://taxsim.nber.org/historical_state_tax_forms/KY/2021/Form%20740%20Packet%20Instructions-2021.pdf#page=27", - # 2020 Form 740 instructions, Schedule M line 5: contributions - # claimed on federal Form 1040 line 10b. - "https://revenue.ky.gov/Forms/740%20Instructions%20Packet%205.7.21.pdf#page=27", ) defined_for = StateCode.KY - - def formula(person, period, parameters): - # The 2020 federal charitable deduction for non-itemizers is added - # back in the part of it that adjusted_gross_income_person (and so - # ky_federal_agi) deducts from each filer. - return person("charitable_contributions_ald_person", period) diff --git a/policyengine_us/variables/gov/states/ky/tax/income/ky_agi.py b/policyengine_us/variables/gov/states/ky/tax/income/ky_agi.py index baf0791882a..00dfde92c1f 100644 --- a/policyengine_us/variables/gov/states/ky/tax/income/ky_agi.py +++ b/policyengine_us/variables/gov/states/ky/tax/income/ky_agi.py @@ -8,12 +8,18 @@ class ky_agi(Variable): unit = USD definition_period = YEAR reference = ( - "https://revenue.ky.gov/Forms/740%20Packet%20Instructions%205-9-23.pdf#page=11" + "https://revenue.ky.gov/Forms/740%20Packet%20Instructions%205-9-23.pdf#page=11", + # 2020 Form 740 instructions, Schedule M line 5: contributions + # claimed on federal Form 1040 line 10b are added back. + "https://revenue.ky.gov/Forms/740%20Instructions%20Packet%205.7.21.pdf#page=27", ) defined_for = StateCode.KY adds = [ "ky_additions", "ky_federal_agi", + # Schedule M line 5 (2020): the federal charitable deduction for + # non-itemizers, in the part ky_federal_agi deducts from each filer. + "charitable_contributions_ald_person", ] subtracts = ["ky_subtractions"] diff --git a/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py b/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py index 98ed4fdb4e1..7f11db33174 100644 --- a/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py +++ b/policyengine_us/variables/gov/states/mn/tax/income/amt/mn_amt_taxable_income.py @@ -15,6 +15,9 @@ class mn_amt_taxable_income(Variable): # 2020 Schedule M1MT instructions, line 4: includes Schedule M1NC # line 6, the federal charitable deduction for non-itemizers. "https://www.revenue.state.mn.us/sites/default/files/2023-02/m1mt_20.pdf#page=2", + # Laws 2023, ch. 1, sec. 22(c)(2) and (d)(2): the addition applies + # to alternative minimum taxable income. + "https://www.revisor.mn.gov/laws/2023/0/Session+Law/Chapter/1/", ) defined_for = StateCode.MN From f919f92edfa8374cbcf466bf3dae66af11a093cf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 00:51:58 -0400 Subject: [PATCH 09/12] Answer Sol review r2: full deduction comparison and NY line 19a credits - Without branching, the 2020 itemization choice compares everything each choice takes from gross income: taxable_income_deductions_if_itemizing (Schedule A, wagering losses, the QBI deduction it allows) against taxable_income_deductions_if_not_itemizing plus the AGI reduction. - New York: get_ny_recomputed_branch, a branch with the 2020 charitable deduction zeroed and the federal itemization choice kept, so the NY and NYC EITC match the federal credit recomputed with IT-201 line 19a (IT-215) and the Empire State child credit's pre-TCJA federal credit uses line 19a (IT-213 Worksheet A). - NYC household, school, child care and income tax elimination credits read ny_recomputed_federal_agi; it is no longer limited to New York units, since NYC tests set residence without the state. Co-Authored-By: Claude Opus 5.5 --- .../charitable_contributions/integration.yaml | 54 +++++++++++++++++++ .../credits/cdcc/nyc_cdcc_eligible.yaml | 24 +++++++++ .../household/nyc_household_credit.yaml | 22 ++++++++ .../ny/nyc/tax/income/credits/nyc_eitc.yaml | 25 +++++++++ .../school/nyc_school_credit_income.yaml | 21 ++++++++ .../tax/income/credits/ny_ctc_pre_2024.yaml | 29 ++++++++++ .../states/ny/tax/income/credits/ny_eitc.yaml | 27 ++++++++++ .../deductions/tax_unit_itemizes.py | 22 ++++---- .../cdcc/nyc_cdcc_applicable_percentage.py | 2 +- .../income/credits/cdcc/nyc_cdcc_eligible.py | 6 +-- .../credits/household/nyc_household_credit.py | 6 +-- .../nyc_income_tax_elimination_credit.py | 2 +- ..._income_tax_elimination_credit_eligible.py | 2 +- .../ny/nyc/tax/income/credits/nyc_eitc.py | 11 +++- .../school/nyc_school_credit_income.py | 5 +- .../ny_recomputed_federal_agi.py | 29 +++++++++- .../tax/income/credits/ctc/ny_ctc_pre_2024.py | 15 +++++- .../states/ny/tax/income/credits/ny_eitc.py | 14 ++++- 18 files changed, 290 insertions(+), 26 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml index 1c1bbfb6651..7d1bd0624eb 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/charitable_contributions/integration.yaml @@ -337,3 +337,57 @@ charitable_contributions_ald: 300 adjusted_gross_income: 59_700 taxable_income: 47_300 + +- name: Case 13, 2020 choice without branching counts wagering losses with Schedule A. + period: 2020 + input: + gov.simulation.branch_to_determine_itemization: false + people: + person1: + age: 40 + employment_income: 60_000 + gambling_winnings: 1_000 + gambling_losses: 1_000 + deductible_mortgage_interest: 12_300 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + # Itemizing takes 12,300 + 300 + 1,000 of wagering losses (Schedule A + # line 16) from income; not itemizing takes 12,400 + 300 (line 10b). + # 13,600 > 12,700, so the unit itemizes. + tax_unit_itemizes: true + wagering_losses_deduction: 1_000 + charitable_contributions_ald: 0 + +- name: Case 14, 2020 choice without branching counts the QBI deduction each choice allows. + period: 2020 + input: + gov.simulation.branch_to_determine_itemization: false + people: + person1: + age: 40 + employment_income: 126_000 + partnership_s_corp_income: 1_000_000 + sstb_self_employment_income: -950_000 + w2_wages_from_qualified_business: 500_000 + deductible_mortgage_interest: 12_300 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NH + output: + # The SSTB loss phases in with taxable income, so the QBI deduction is + # $10,380 when itemizing and $10,000 when not (Sol review r2). Itemizing + # leaves taxable income $153,020 against $153,300 without. + tax_unit_itemizes: true + taxable_income: 153_020 diff --git a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.yaml index 8663b817733..b2701520f8d 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.yaml @@ -33,3 +33,27 @@ adjusted_gross_income: 30_001 output: nyc_cdcc_eligible: false + +- name: 2020 NYC child care credit tests recomputed federal AGI + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 30_100 + charitable_cash_donations: 300 + person2: + age: 3 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NY + in_nyc: true + output: + # Federal AGI 29,800 is under $30,000; recomputed federal AGI (IT-216 + # line 9, IT-201 line 19a) is 30,100, over it. + adjusted_gross_income: 29_800 + nyc_cdcc_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.yaml b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.yaml index 43eeabb49b8..badd48577b1 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.yaml @@ -349,3 +349,25 @@ output: # $30 x 2. nyc_household_credit: 60 + +- name: 2020 NYC household credit uses recomputed federal AGI + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 12_600 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + in_nyc: true + output: + # Federal AGI 12,300 is under the $12,500 single limit, but recomputed + # federal AGI (IT-201 line 19a) is 12,600, over it. + adjusted_gross_income: 12_300 + nyc_household_credit: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/nyc_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/nyc_eitc.yaml index 10c2d656008..5b84c2e9e0a 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/nyc_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/nyc_eitc.yaml @@ -67,3 +67,28 @@ # 0.18 # 3. Allowable NYC EIC (multiply line 1 by line 2) nyc_eitc: 18 + +- name: 2020 NYC EITC matches the federal credit recomputed with line 19a + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 40_800 + taxable_interest_income: 1_000 + charitable_cash_donations: 300 + person2: + age: 10 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NY + in_nyc: true + output: + # Recomputed federal AGI 41,800 is over the $41,756 one-child limit. + ny_recomputed_federal_agi: 41_800 + nyc_eitc: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.yaml b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.yaml index f0dff0043a9..590b2877469 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.yaml @@ -108,3 +108,24 @@ # distribution and income equals AGI. adjusted_gross_income: 300_000 nyc_school_credit_income: 300_000 + +- name: 2020 NYC school credit income uses recomputed federal AGI + period: 2020 + input: + people: + person1: + age: 40 + employment_income: 250_100 + charitable_cash_donations: 300 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + in_nyc: true + output: + # Federal AGI 249,800; recomputed federal AGI (line 19a) 250,100. + adjusted_gross_income: 249_800 + nyc_school_credit_income: 250_100 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml index d083f389a8d..d205f33658c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml @@ -97,3 +97,32 @@ output: ny_ctc: 330 ny_ctc_pre_2024: 330 + +- name: 2020 Empire State child credit recomputes the federal credit with line 19a + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 110_100 + charitable_cash_donations: 300 + person2: + age: 40 + person3: + age: 10 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: NY + output: + # Federal AGI 110,100 - 300 = 109,800; IT-201 line 19a adds the 300 back. + adjusted_gross_income: 109_800 + ny_recomputed_federal_agi: 110_100 + # IT-213 Worksheet A (2020 instructions, file page 2) with line 19a: + # 110,100 is over the $110,000 joint threshold; the excess rounds up to + # $1,000, so 5% = $50 off the $1,000 credit; 33% x 950 = 313.50. + ny_ctc_pre_2024: 313.5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml index 8ff2589fd60..56bdd57f9fc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml @@ -199,3 +199,30 @@ # 543 - 7.65% x (12,000 - 8,880) = 304.32; 30% = 91.30; less the $45 # household credit (table 1, 7,000-20,000) = 46.30 ny_eitc: 46.30 + +- name: 2020 NY EITC matches the federal credit recomputed with line 19a + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 40_800 + taxable_interest_income: 1_000 + charitable_cash_donations: 300 + person2: + age: 10 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NY + output: + # Federal AGI 41,800 - 300 = 41,500, under the $41,756 one-child limit; + # recomputed federal AGI (line 19a) is 41,800, over it (2020 IT-215 + # instructions, file page 2: recompute the federal credit with line 19a). + adjusted_gross_income: 41_500 + ny_recomputed_federal_agi: 41_800 + ny_eitc: 0 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py index 78ae5ec6e77..3af748cde7d 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py @@ -22,19 +22,23 @@ def formula(tax_unit, period, parameters): # that also give the tax liabilities compared below). itemizing = get_itemization_branch(tax_unit, period, itemizes=True) not_itemizing = get_itemization_branch(tax_unit, period, itemizes=False) - itemized_deductions = itemizing.calculate( - "itemized_taxable_income_deductions", period - ) - # Not itemizing also lowers adjusted gross income (line 10b, and - # what follows from it, such as taxable Social Security), so the - # deductions compared are everything each choice takes from - # gross income: the standard deduction counts with that - # reduction. + # The deductions compared are everything each choice takes from + # gross income: every deduction from adjusted gross income on + # that side (with the qualified business income deduction and, + # for an itemizer, wagering losses), and, for not itemizing, the + # lower adjusted gross income (line 10b and what follows from it, + # such as taxable Social Security). agi_reduction = itemizing.calculate( "adjusted_gross_income", period ) - not_itemizing.calculate("adjusted_gross_income", period) + itemized_deductions = itemizing.calculate( + "taxable_income_deductions_if_itemizing", period + ) standard_deduction = ( - not_itemizing.calculate("standard_deduction", period) + agi_reduction + not_itemizing.calculate( + "taxable_income_deductions_if_not_itemizing", period + ) + + agi_reduction ) elif limitation.applies and limitation.obbb.applies: standard_deduction = tax_unit("standard_deduction", period) diff --git a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_applicable_percentage.py b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_applicable_percentage.py index 2bec01f1e12..be2606ee535 100644 --- a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_applicable_percentage.py +++ b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_applicable_percentage.py @@ -17,7 +17,7 @@ def formula(tax_unit, period, parameters): # § 11-1706 Credits against tax (e). # First get federal AGI. - income = tax_unit("adjusted_gross_income", period) + income = tax_unit("ny_recomputed_federal_agi", period) # Then get the CDCC part of the parameter tree. p = parameters(period).gov.local.ny.nyc.tax.income.credits.cdcc diff --git a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.py b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.py index 0724bfa2e38..b028a199b7f 100644 --- a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.py +++ b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/cdcc/nyc_cdcc_eligible.py @@ -9,11 +9,11 @@ class nyc_cdcc_eligible(Variable): defined_for = "in_nyc" def formula(tax_unit, period, parameters): - # Eligibility is based on having a federal AGI below $30k - # and being an NYC full-time resident. + # Eligibility is based on having a recomputed federal AGI (IT-201 + # line 19a) below $30k and being an NYC full-time resident. # First get their income. - income = tax_unit("adjusted_gross_income", period) + income = tax_unit("ny_recomputed_federal_agi", period) # Then get the CDCC part of the parameter tree. p = parameters(period).gov.local.ny.nyc.tax.income.credits.cdcc diff --git a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.py b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.py index c919d1626d0..3f470e086f5 100644 --- a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.py +++ b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/household/nyc_household_credit.py @@ -10,9 +10,9 @@ class nyc_household_credit(Variable): defined_for = "in_nyc" def formula(tax_unit, period, parameters): - # First get their federal AGI. - # Technically based on recomputed AGI, which deviates slightly from federal AGI. - federal_agi = tax_unit("adjusted_gross_income", period) + # Recomputed federal AGI (IT-201 line 19a), which differs from + # federal AGI in 2020 by the charitable deduction for non-itemizers. + federal_agi = tax_unit("ny_recomputed_federal_agi", period) # Then get their filing status. filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit.py b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit.py index 96bf0cd4f4b..7af2af6eb63 100644 --- a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit.py +++ b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit.py @@ -22,7 +22,7 @@ def formula(tax_unit, period, parameters): # proportion to federal adjusted gross income above the threshold and # reaches zero at the phase-out width. Line 5 rounds the fraction to # the fourth decimal place. - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) threshold = tax_unit( "nyc_income_tax_elimination_credit_income_threshold", period ) diff --git a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit_eligible.py b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit_eligible.py index ba473a6818c..995dfeb3dbb 100644 --- a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit_eligible.py +++ b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/income_tax_elimination/nyc_income_tax_elimination_credit_eligible.py @@ -28,7 +28,7 @@ def formula(tax_unit, period, parameters): # § 1310(h)(1)(B) and (h)(2); line C: federal adjusted gross income # (§ 1310(h)(3)(B)) is no more than the phase-out width above the # threshold. - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) threshold = tax_unit( "nyc_income_tax_elimination_credit_income_threshold", period ) diff --git a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/nyc_eitc.py b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/nyc_eitc.py index 774f7369abc..99a817c8382 100644 --- a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/nyc_eitc.py +++ b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/nyc_eitc.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ny.tax.income.adjusted_gross_income.ny_recomputed_federal_agi import ( + get_ny_recomputed_branch, +) class nyc_eitc(Variable): @@ -12,7 +15,13 @@ class nyc_eitc(Variable): def formula(tax_unit, period, parameters): # First get their NYAGI and EITC. ny_agi = tax_unit("ny_agi", period) - federal_eitc = tax_unit("eitc", period) + # The federal credit recomputed with recomputed federal AGI (IT-201 + # line 19a; IT-215 instructions) when it differs from federal AGI. + branch = get_ny_recomputed_branch(tax_unit, period) + if branch is None: + federal_eitc = tax_unit("eitc", period) + else: + federal_eitc = branch.calculate("eitc", period) # Then get the EITC part of the parameter tree. p = parameters(period).gov.local.ny.nyc.tax.income.credits.eitc diff --git a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.py b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.py index 71e1de010c6..6a6e52d3106 100644 --- a/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.py +++ b/policyengine_us/variables/gov/local/ny/nyc/tax/income/credits/school/nyc_school_credit_income.py @@ -21,12 +21,11 @@ def formula(tax_unit, period, parameters): # adjusted gross income reduced by distributions from individual # retirement accounts and annuities, to the extent included in # federal adjusted gross income (IT-201 line 19 minus line 9; the - # 2022 instructions used recomputed federal AGI, line 19a, which is - # not modeled separately). + # 2020 and 2022 instructions use recomputed federal AGI, line 19a). # Roth conversions are not identified as IRA-sourced, and individual # retirement annuity distributions recorded as pension income are not # identified separately, so both stay in income. - agi = tax_unit("adjusted_gross_income", period) + agi = tax_unit("ny_recomputed_federal_agi", period) p = parameters(period).gov.local.ny.nyc.tax.income.credits.school person = tax_unit.members # Federal gross income excludes dependents' income, so only the diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py index cfb3102a5b0..7eb8bdbf30f 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py @@ -1,6 +1,32 @@ from policyengine_us.model_api import * +def get_ny_recomputed_branch(tax_unit, period): + """ + The branch whose federal adjusted gross income is New York's recomputed + federal AGI (Form IT-201 line 19a), or None when the two are equal for + every tax unit. + + New York matches federal credits computed with line 19a (the IT-213 and + IT-215 recomputations). The only Form IT-558 adjustment modeled is A-003, + the 2020 federal charitable deduction for non-itemizers, so the branch + sets that deduction to zero and keeps each unit's federal itemization + choice. + """ + deduction = tax_unit("charitable_contributions_ald", period) + if not deduction.any(): + return None + return get_override_branch( + tax_unit.simulation, + "ny_recomputed_federal_agi", + period, + { + "charitable_contributions_ald": np.zeros(tax_unit.count), + "tax_unit_itemizes": tax_unit("tax_unit_itemizes", period), + }, + ) + + class ny_recomputed_federal_agi(Variable): value_type = float entity = TaxUnit @@ -21,6 +47,7 @@ class ny_recomputed_federal_agi(Variable): # 2020 IT-558 instructions, addition A-003. "https://www.tax.ny.gov/pdf/2020/inc/it558i_2020.pdf#page=2", ) - defined_for = StateCode.NY + # Not limited to New York: its readers are, and New York City residents + # are New York residents. adds = ["adjusted_gross_income", "charitable_contributions_ald"] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py index 31f14cdbc20..12e55b87ef0 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py @@ -3,6 +3,9 @@ CREDIT_DEPENDENT_VARIABLES, get_pre_tcja_ctc_tbs, ) +from policyengine_us.variables.gov.states.ny.tax.income.adjusted_gross_income.ny_recomputed_federal_agi import ( + get_ny_recomputed_branch, +) class ny_ctc_pre_2024(Variable): @@ -14,7 +17,11 @@ class ny_ctc_pre_2024(Variable): ) unit = USD definition_period = YEAR - reference = "https://www.nysenate.gov/legislation/laws/TAX/606" # (c-1) + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/606", # (c-1) + # 2020 IT-213 instructions: the credit uses recomputed federal AGI. + "https://www.tax.ny.gov/pdf/2020/inc/it213i_2020.pdf#page=2", + ) defined_for = "ny_ctc_pre_2024_eligible" def formula(tax_unit, period, parameters): @@ -31,7 +38,11 @@ def formula(tax_unit, period, parameters): else: # Initialize pre-TCJA CTC branch with cached pinned parameters # (one clone per process; see tools/pinned_tbs.py, issue #8114). - simulation = tax_unit.simulation + # IT-213 Worksheet A computes the federal credit with recomputed + # federal AGI (IT-201 line 19a), so the branch starts from the + # one where federal AGI is line 19a when they differ. + recomputed = get_ny_recomputed_branch(tax_unit, period) + simulation = tax_unit.simulation if recomputed is None else recomputed pre_tcja_ctc = get_branch_for_period(simulation, "pre_tcja_ctc", period) pre_tcja_ctc.tax_benefit_system = get_pre_tcja_ctc_tbs( simulation.tax_benefit_system diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py index 20c38b23cdb..c2abee94cf2 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * from policyengine_us.tools.pinned_tbs import get_pre_arpa_eitc_tbs +from policyengine_us.variables.gov.states.ny.tax.income.adjusted_gross_income.ny_recomputed_federal_agi import ( + get_ny_recomputed_branch, +) class ny_eitc(Variable): @@ -11,6 +14,9 @@ class ny_eitc(Variable): reference = ( "https://www.nysenate.gov/legislation/laws/TAX/606", # (d) "https://www.tax.ny.gov/pdf/2021/inc/it215i_2021.pdf", + # 2020 IT-215 instructions: recompute the federal credit with line + # 19a when lines 19 and 19a differ. + "https://www.tax.ny.gov/pdf/2020/inc/it215i_2020.pdf#page=2", ) defined_for = StateCode.NY @@ -32,7 +38,13 @@ def formula(tax_unit, period, parameters): branch.delete_arrays(variable) federal_eitc = branch.tax_unit("eitc", period) else: - federal_eitc = tax_unit("eitc", period) + # IT-215: the federal credit recomputed with recomputed federal + # AGI (IT-201 line 19a) when it differs from federal AGI. + branch = get_ny_recomputed_branch(tax_unit, period) + if branch is None: + federal_eitc = tax_unit("eitc", period) + else: + federal_eitc = branch.calculate("eitc", period) tentative_nys_eitc = federal_eitc * p.eitc.match income_tax_before_credits = tax_unit("ny_income_tax_before_credits", period) household_credit = tax_unit("ny_household_credit", period) From 5094dac5bb9db0185f96e3adde017b7ae0b60527 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 01:23:34 -0400 Subject: [PATCH 10/12] Answer Sol review r3: line 19a adds only the deduction - The New York branch sets adjusted gross income to line 19a (federal AGI as reported plus IT-558 A-003) instead of zeroing the deduction and recomputing, which also moved taxable Social Security; this also covers a supplied adjusted_gross_income. - For the Empire State child credit it keeps income tax before credits as reported (IT-213 Worksheet A lines 7-8); the EITC branch does not read it, because New York City income tax enters the federal SALT deduction. Co-Authored-By: Claude Opus 5.5 --- .../tax/income/credits/ny_ctc_pre_2024.yaml | 57 +++++++++++++++++++ .../ny_recomputed_federal_agi.py | 39 ++++++++----- .../tax/income/credits/ctc/ny_ctc_pre_2024.py | 4 +- 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml index d205f33658c..0080bb5e280 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml @@ -126,3 +126,60 @@ # 110,100 is over the $110,000 joint threshold; the excess rounds up to # $1,000, so 5% = $50 off the $1,000 credit; 33% x 950 = 313.50. ny_ctc_pre_2024: 313.5 + +- name: 2020 Empire State child credit adds only the deduction to federal AGI, recomputing nothing else + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 67 + employment_income: 62_690 + social_security_retirement: 30_000 + charitable_cash_donations: 300 + person2: + age: 67 + social_security_retirement: 30_000 + person3: + age: 10 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: NY + output: + # Line 19a = federal AGI as reported (with the taxable Social Security + # figured after line 10b) plus the 300: 109,521.50 + 300 = 109,821.50, + # under the $110,000 joint threshold, so 33% of the full $1,000 (Sol + # review r3). + adjusted_gross_income: 109_521.5 + ny_recomputed_federal_agi: 109_821.5 + ny_ctc_pre_2024: 330 + +- name: 2020 Empire State child credit keeps the federal tax as reported + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + taxable_pension_income: 23_030 + charitable_cash_donations: 300 + person2: + age: 10 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NY + output: + # IT-213 Worksheet A lines 7-8 take Form 1040 line 18 as reported: the + # federal tax is $408 (AGI 22,730, head of household), which limits the + # nonrefundable $1,000 credit; there are no earnings for the refundable + # part. 33% x 408 = 134.64 (Sol review r3). + income_tax_before_credits: 408 + ny_ctc_pre_2024: 134.64 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py index 7eb8bdbf30f..4f7555a86e5 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py @@ -1,30 +1,41 @@ from policyengine_us.model_api import * -def get_ny_recomputed_branch(tax_unit, period): +def get_ny_recomputed_branch(tax_unit, period, keep_federal_tax=False): """ The branch whose federal adjusted gross income is New York's recomputed federal AGI (Form IT-201 line 19a), or None when the two are equal for every tax unit. New York matches federal credits computed with line 19a (the IT-213 and - IT-215 recomputations). The only Form IT-558 adjustment modeled is A-003, - the 2020 federal charitable deduction for non-itemizers, so the branch - sets that deduction to zero and keeps each unit's federal itemization - choice. + IT-215 recomputations). Line 19a is federal AGI as reported plus the Form + IT-558 additions, of which the model has one, A-003, the 2020 federal + charitable deduction for non-itemizers. Nothing else on the federal + return is recomputed, so the branch sets adjusted gross income to line + 19a and keeps each unit's federal itemization choice. The deduction is + zero in the branch, which already counts it in adjusted gross income. + + With ``keep_federal_tax``, the branch also keeps income tax before + credits (Form 1040 line 18), which IT-213 Worksheet A takes as reported. + The EITC does not need it, and New York City's EITC must not read it: + New York City income tax enters the federal deduction for state and + local taxes. """ deduction = tax_unit("charitable_contributions_ald", period) if not deduction.any(): return None - return get_override_branch( - tax_unit.simulation, - "ny_recomputed_federal_agi", - period, - { - "charitable_contributions_ald": np.zeros(tax_unit.count), - "tax_unit_itemizes": tax_unit("tax_unit_itemizes", period), - }, - ) + overrides = { + "adjusted_gross_income": tax_unit("ny_recomputed_federal_agi", period), + "charitable_contributions_ald": np.zeros(tax_unit.count), + "tax_unit_itemizes": tax_unit("tax_unit_itemizes", period), + } + name = "ny_recomputed_federal_agi" + if keep_federal_tax: + overrides["income_tax_before_credits"] = tax_unit( + "income_tax_before_credits", period + ) + name = "ny_recomputed_federal_agi_with_federal_tax" + return get_override_branch(tax_unit.simulation, name, period, overrides) class ny_recomputed_federal_agi(Variable): diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py index 12e55b87ef0..15899a4afb1 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py @@ -41,7 +41,9 @@ def formula(tax_unit, period, parameters): # IT-213 Worksheet A computes the federal credit with recomputed # federal AGI (IT-201 line 19a), so the branch starts from the # one where federal AGI is line 19a when they differ. - recomputed = get_ny_recomputed_branch(tax_unit, period) + recomputed = get_ny_recomputed_branch( + tax_unit, period, keep_federal_tax=True + ) simulation = tax_unit.simulation if recomputed is None else recomputed pre_tcja_ctc = get_branch_for_period(simulation, "pre_tcja_ctc", period) pre_tcja_ctc.tax_benefit_system = get_pre_tcja_ctc_tbs( From 14a8d24d27b316d03e11dff8a1cb2891e1f71aa0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 01:51:06 -0400 Subject: [PATCH 11/12] Carry the federal EITC as reported into the Empire State child credit IT-213 Worksheet C line 7 carries federal Schedule 8812 line 13, whose lines 12-13 use the earned income credit as reported, so the child credit's branch keeps eitc with the reported federal tax (Sol review r4). Co-Authored-By: Claude Opus 5.5 --- .../tax/income/credits/ny_ctc_pre_2024.yaml | 37 +++++++++++++++++++ .../ny_recomputed_federal_agi.py | 19 ++++++---- .../tax/income/credits/ctc/ny_ctc_pre_2024.py | 2 +- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml index 0080bb5e280..03b750ec757 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml @@ -183,3 +183,40 @@ # part. 33% x 408 = 134.64 (Sol review r3). income_tax_before_credits: 408 ny_ctc_pre_2024: 134.64 + +- name: 2020 Empire State child credit carries the federal EITC as reported + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 67 + employment_income: 3_000 + taxable_pension_income: 52_925 + charitable_cash_donations: 300 + person2: + age: 67 + person3: + age: 10 + person4: + age: 10 + person5: + age: 10 + person6: + age: 10 + tax_units: + tax_unit: + members: [person1, person2, person3, person4, person5, person6] + households: + household: + members: [person1, person2, person3, person4, person5, person6] + state_code: NY + output: + adjusted_gross_income: 55_625 + ny_recomputed_federal_agi: 55_925 + income_tax_before_credits: 2_992 + # IT-213 Worksheet C line 7 carries federal Schedule 8812 line 13 as + # reported: the federal EITC as reported exceeds the $229.50 of payroll + # taxes, so the refundable part is zero, and the credit is 33% of the + # $2,992 of tax the nonrefundable part is limited to (Sol review r4). + ny_ctc_pre_2024: 987.36 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py index 4f7555a86e5..c48ea7adf07 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/adjusted_gross_income/ny_recomputed_federal_agi.py @@ -1,7 +1,7 @@ from policyengine_us.model_api import * -def get_ny_recomputed_branch(tax_unit, period, keep_federal_tax=False): +def get_ny_recomputed_branch(tax_unit, period, keep_reported_tax_and_eitc=False): """ The branch whose federal adjusted gross income is New York's recomputed federal AGI (Form IT-201 line 19a), or None when the two are equal for @@ -15,11 +15,13 @@ def get_ny_recomputed_branch(tax_unit, period, keep_federal_tax=False): 19a and keeps each unit's federal itemization choice. The deduction is zero in the branch, which already counts it in adjusted gross income. - With ``keep_federal_tax``, the branch also keeps income tax before - credits (Form 1040 line 18), which IT-213 Worksheet A takes as reported. - The EITC does not need it, and New York City's EITC must not read it: - New York City income tax enters the federal deduction for state and - local taxes. + With ``keep_reported_tax_and_eitc``, for the Empire State child credit, + the branch also keeps income tax before credits (Form 1040 line 18, + which IT-213 Worksheet A takes as reported) and the federal earned income + credit (which Schedule 8812 lines 12-13, carried by IT-213 Worksheet C + line 7, take as reported). The New York and New York City EITC branch + does not read the tax: New York City income tax enters the federal + deduction for state and local taxes. """ deduction = tax_unit("charitable_contributions_ald", period) if not deduction.any(): @@ -30,11 +32,12 @@ def get_ny_recomputed_branch(tax_unit, period, keep_federal_tax=False): "tax_unit_itemizes": tax_unit("tax_unit_itemizes", period), } name = "ny_recomputed_federal_agi" - if keep_federal_tax: + if keep_reported_tax_and_eitc: overrides["income_tax_before_credits"] = tax_unit( "income_tax_before_credits", period ) - name = "ny_recomputed_federal_agi_with_federal_tax" + overrides["eitc"] = tax_unit("eitc", period) + name = "ny_recomputed_federal_agi_for_child_credit" return get_override_branch(tax_unit.simulation, name, period, overrides) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py index 15899a4afb1..b032548edef 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py @@ -42,7 +42,7 @@ def formula(tax_unit, period, parameters): # federal AGI (IT-201 line 19a), so the branch starts from the # one where federal AGI is line 19a when they differ. recomputed = get_ny_recomputed_branch( - tax_unit, period, keep_federal_tax=True + tax_unit, period, keep_reported_tax_and_eitc=True ) simulation = tax_unit.simulation if recomputed is None else recomputed pre_tcja_ctc = get_branch_for_period(simulation, "pre_tcja_ctc", period) From 5cbab1aac5db9c994ab37d3e8d69859f51283675 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 12:41:37 -0400 Subject: [PATCH 12/12] Count non-itemizer charitable deductions when choosing itemization --- changelog.d/fix-2020-charitable-ald.fixed.md | 2 +- ...charitable_contributions_ald_invariants.py | 3 +- .../deductions/tax_unit_itemizes.yaml | 58 +++++++++++++++++++ .../deductions/tax_unit_itemizes.py | 12 +++- 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/changelog.d/fix-2020-charitable-ald.fixed.md b/changelog.d/fix-2020-charitable-ald.fixed.md index e809f6e210d..13e1235cfb5 100644 --- a/changelog.d/fix-2020-charitable-ald.fixed.md +++ b/changelog.d/fix-2020-charitable-ald.fixed.md @@ -1 +1 @@ -Take the 2020 charitable deduction for non-itemizers (IRC 62(a)(22), Form 1040 line 10b) above the line, capped at $300 for every return as the statute reads, so it reduces adjusted gross income; stop adding the 2020 and 2021 non-itemizer amount to itemized deductions; and add it back in states that did not adopt it. +Take the 2020 charitable deduction for non-itemizers (IRC 62(a)(22), Form 1040 line 10b) above the line, capped at $300 for every return as the statute reads, so it reduces adjusted gross income; stop adding the 2020 and 2021 non-itemizer amount to itemized deductions; include the non-itemizer cash charitable deduction in itemization comparisons; and add it back in states that did not adopt it. diff --git a/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py b/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py index 2f597f04e40..d8f6d201e9d 100644 --- a/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py +++ b/policyengine_us/tests/core/test_charitable_contributions_ald_invariants.py @@ -214,8 +214,7 @@ def calc(variable): ma = in_state("MA") np.testing.assert_allclose(calc("ma_part_b_agi")[ma], wages[ma], atol=1) - # 6. Without branching, the choice leaves the lower taxable income. (In - # 2021 the comparison ignores the 170(p) deduction, a separate issue.) + # 6. Without branching, the choice leaves the lower taxable income. if without_branching: plain = Simulation(situation=_situation(units, year), reform=NoBranching) taxable_income = np.asarray(plain.calculate("taxable_income", year)) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.yaml index 90e8017e8c4..63799f86996 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.yaml @@ -55,6 +55,64 @@ output: tax_unit_itemizes: False +- name: 2021 without branching - compare itemized deductions with standard plus cash gifts + period: 2021 + input: + gov.simulation.branch_to_determine_itemization: false + # Each triple is below, equal to, and above the 2021 Form 1040 line 12c + # total: standard deduction plus the cash gift allowed by 26 U.S.C. 170(p). + # Singles have full, partial, then zero gifts; joint filers repeat these. + filing_status: [SINGLE, SINGLE, SINGLE, SINGLE, SINGLE, SINGLE, SINGLE, SINGLE, SINGLE, JOINT, JOINT, JOINT, JOINT, JOINT, JOINT, JOINT, JOINT, JOINT] + standard_deduction: [12_550, 12_550, 12_550, 12_550, 12_550, 12_550, 12_550, 12_550, 12_550, 25_100, 25_100, 25_100, 25_100, 25_100, 25_100, 25_100, 25_100, 25_100] + charitable_cash_donations: [400, 400, 400, 150, 150, 150, 0, 0, 0, 700, 700, 700, 250, 250, 250, 0, 0, 0] + itemized_taxable_income_deductions: [12_849, 12_850, 12_851, 12_699, 12_700, 12_701, 12_549, 12_550, 12_551, 25_699, 25_700, 25_701, 25_349, 25_350, 25_351, 25_099, 25_100, 25_101] + output: + # Section 170(p): $300 per single return and $600 per joint return, + # limited to the eligible cash actually given. + charitable_deduction_for_non_itemizers: [300, 300, 300, 150, 150, 150, 0, 0, 0, 600, 600, 600, 250, 250, 250, 0, 0, 0] + tax_unit_itemizes: [false, false, true, false, false, true, false, false, true, false, false, true, false, false, true, false, false, true] + +- name: 2021 equal federal tax - deduction comparison includes the non-itemizer cash deduction + period: 2021 + input: + gov.simulation.branch_to_determine_itemization: true + filing_status: SINGLE + standard_deduction: 12_550 + charitable_cash_donations: 300 + itemized_taxable_income_deductions: 12_699 + tax_liability_if_itemizing: 0 + tax_liability_if_not_itemizing: 0 + output: + # 2021 Form 1040 line 12c: 12_550 + 300 under section 170(p) = 12_850. + tax_unit_itemizes: false + +- name: 2026 without branching - the cash deduction makes not itemizing preferable + period: 2026 + input: + gov.simulation.branch_to_determine_itemization: false + people: + head: + age: 45 + employment_income: 60_000 + charitable_cash_donations: 1_000 + deductible_mortgage_interest: 15_800 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NH + output: + # Section 170(b)(1)(I): 1_000 - 0.005 * 60_000 = 700 itemized gifts. + charitable_deduction: 700 + itemized_taxable_income_deductions: 16_500 + standard_deduction: 16_100 + # Section 170(p): the non-itemizer instead deducts the full 1_000. + charitable_deduction_for_non_itemizers: 1_000 + tax_unit_itemizes: false + taxable_income: 42_900 + - name: 2026 - the itemization choice uses the section 68 limitation net of the QBI deduction period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py index 3af748cde7d..30e413249e2 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py @@ -41,7 +41,11 @@ def formula(tax_unit, period, parameters): + agi_reduction ) elif limitation.applies and limitation.obbb.applies: - standard_deduction = tax_unit("standard_deduction", period) + standard_deduction = add( + tax_unit, + period, + ["standard_deduction", "charitable_deduction_for_non_itemizers"], + ) # The itemized deductions the unit would claim if it itemized. # The P.L. 119-21 section 68 limitation nets them of the # qualified business income deduction, which depends on the @@ -53,7 +57,11 @@ def formula(tax_unit, period, parameters): tax_unit, period, itemizes=True ).calculate("itemized_taxable_income_deductions", period) else: - standard_deduction = tax_unit("standard_deduction", period) + standard_deduction = add( + tax_unit, + period, + ["standard_deduction", "charitable_deduction_for_non_itemizers"], + ) itemized_deductions = tax_unit("itemized_taxable_income_deductions", period) if parameters(period).gov.simulation.branch_to_determine_itemization: # determine federal itemization behavior by comparing tax liability