From 9ea8aac872a8abb47517e3b33353bcabfadf1982 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 09:43:49 -0400 Subject: [PATCH 1/4] Leave dependents' capital income out of state lines on the filers' return A tax unit dependent's interest, dividends and capital gains are on the dependent's own return, and federal adjusted gross income (irs_gross_income) leaves them out. State lines that adjust the filers' federal AGI or taxable income summed them over every member, so a child's dividends, gains or interest moved the parents' state tax. Count only the head and spouse (tax_unit_non_dep_add, or a dependent mask on per-person lines) in AZ, DC, DE, IA, IL, MA, MD, ME, MN, MT, ND, NH, OH, PA, SC, UT, VA, VT, WA, WI and WV, and in us_govt_interest, which most states subtract. In formulas edited anyway: North Dakota's net long-term capital gain exclusion is floored at zero and includes capital gain distributions (Form ND-1 line 6 worksheet); Utah's retirement credit counts only the head and spouse as claimants; the West Virginia low-income family credit cites W. Va. Code 11-21-22a(d). Co-Authored-By: Claude Opus 5.5 --- ...endent-capital-income-state-lines.fixed.md | 1 + ...ent_capital_income_state_tax_invariants.py | 315 ++++++++++++++++++ ...z_long_term_capital_gains_subtraction.yaml | 34 ++ .../dc/tax/income/dc_income_subtractions.yaml | 18 + .../income/subtractions/de_subtractions.yaml | 18 + .../income/ia_reportable_social_security.yaml | 34 ++ .../income/il_base_income_additions.yaml | 16 + .../gross_income/ma_part_a_gross_income.yaml | 20 ++ .../gross_income/ma_part_c_gross_income.yaml | 36 ++ .../md_capital_gains_surtax.yaml | 17 + ...d_property_tax_fairness_credit_income.yaml | 34 ++ .../mn_social_security_subtraction.yaml | 34 ++ .../mt_capital_gains_tax_joint.yaml | 39 +++ .../credits/mt_capital_gain_credit.yaml | 17 + .../mt_interest_exemption_person.yaml | 18 + .../states/mt/tax/income/mt_agi_joint.yaml | 18 + .../mt_taxable_social_security_joint.yaml | 35 ++ .../nd/tax/income/nd_ltcg_subtraction.yaml | 65 ++++ .../nd/tax/income/nd_qdiv_subtraction.yaml | 17 + .../nh/tax/income/nh_taxable_income.yaml | 18 + .../tax/income/deductions/oh_deductions.yaml | 18 + .../forgiveness/pa_eligibility_income.yaml | 34 ++ .../sc_net_capital_gain_deduction.yaml | 17 + .../ut_retirement_credit_max.yaml | 17 + .../ut_ss_benefits_credit_max.yaml | 33 ++ .../tax/income/credits/ut_ctc_potential.yaml | 15 + .../states/va/tax/income/va_subtractions.yaml | 19 ++ .../vt/tax/income/agi/vt_additions.yaml | 16 + ...vt_percentage_capital_gains_exclusion.yaml | 40 +++ .../wa/tax/income/wa_capital_gains_tax.yaml | 17 + .../states/wi/tax/income/wi_additions.yaml | 16 + .../wi_capital_gain_loss_subtration.yaml | 17 + .../states/wi/tax/income/wi_capital_loss.yaml | 17 + .../wv_low_income_family_tax_credit_agi.yaml | 17 + .../income/tax_unit/us_govt_interest.yaml | 18 + .../az_long_term_capital_gains_subtraction.py | 13 +- .../subtractions/dc_income_subtractions.py | 9 + .../income/subtractions/de_subtractions.py | 9 + .../income/ia_reportable_social_security.py | 7 +- .../base_income/il_base_income_additions.py | 8 +- .../adjusted_gross_income/ma_part_a_agi.py | 15 +- .../gross_income/ma_part_a_gross_income.py | 7 +- .../gross_income/ma_part_c_gross_income.py | 7 +- .../ma_part_a_cg_excess_exemption.py | 3 +- .../ma_part_a_div_excess_exemption.py | 3 +- .../ma_part_a_taxable_capital_gains_income.py | 3 +- .../ma_part_a_taxable_dividend_income.py | 3 +- .../ma_part_b_taxable_income_deductions.py | 6 +- .../capital_gains/md_capital_gains_surtax.py | 4 +- ...and_property_tax_fairness_credit_income.py | 11 +- .../mn_social_security_subtraction.py | 13 +- .../states/mt/tax/income/base/mt_agi_joint.py | 13 +- .../base/mt_taxable_social_security_joint.py | 13 +- ...al_gains_tax_applicable_threshold_joint.py | 6 +- .../mt_capital_gains_tax_joint.py | 6 +- .../capital_gain/mt_capital_gain_credit.py | 5 +- .../interest/mt_interest_exemption_person.py | 6 +- .../mt_regular_income_tax_joint.py | 6 +- .../subtractions/nd_ltcg_subtraction.py | 22 +- .../subtractions/nd_qdiv_subtraction.py | 8 +- .../states/nh/tax/income/nh_taxable_income.py | 13 +- .../oh/tax/income/deductions/oh_deductions.py | 15 +- .../forgiveness/pa_eligibility_income.py | 13 +- .../sc_net_capital_gain_deduction.py | 8 +- .../child_tax_credit/ut_ctc_potential.py | 4 +- .../ut_retirement_credit_max.py | 14 +- .../ut_ss_benefits_credit_max.py | 4 +- .../states/va/tax/income/va_subtractions.py | 5 +- .../va/tax/income/va_subtractions_person.py | 5 +- .../vt_percentage_capital_gains_exclusion.py | 6 +- .../adjusted_gross_income/vt_additions.py | 7 +- .../wa_millionaires_tax_base_income.py | 10 +- .../wa/tax/income/wa_capital_gains_tax.py | 5 +- .../wi/tax/income/additions/wi_additions.py | 9 +- .../tax/income/additions/wi_capital_loss.py | 4 +- .../wi_capital_gain_loss_subtraction.py | 7 +- .../wv_low_income_family_tax_credit_agi.py | 19 +- .../income/tax_unit/us_govt_interest.py | 17 +- 78 files changed, 1406 insertions(+), 80 deletions(-) create mode 100644 changelog.d/dependent-capital-income-state-lines.fixed.md create mode 100644 policyengine_us/tests/core/test_dependent_capital_income_state_tax_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/il/tax/income/income/il_base_income_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_and_property_tax_fairness_credit_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_joint.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_taxable_social_security_joint.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nh/tax/income/nh_taxable_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_ctc_potential.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_additions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.yaml create mode 100644 policyengine_us/tests/policy/baseline/household/income/tax_unit/us_govt_interest.yaml diff --git a/changelog.d/dependent-capital-income-state-lines.fixed.md b/changelog.d/dependent-capital-income-state-lines.fixed.md new file mode 100644 index 00000000000..cf87b71e125 --- /dev/null +++ b/changelog.d/dependent-capital-income-state-lines.fixed.md @@ -0,0 +1 @@ +Leave tax unit dependents' interest, dividends and capital gains out of the state lines that adjust the filers' federal adjusted gross income or taxable income (AZ, DC, DE, IA, IL, MA, MD, ME, MN, MT, ND, NH, OH, PA, SC, UT, VA, VT, WA, WI and WV), and out of the U.S. government interest that most states subtract. diff --git a/policyengine_us/tests/core/test_dependent_capital_income_state_tax_invariants.py b/policyengine_us/tests/core/test_dependent_capital_income_state_tax_invariants.py new file mode 100644 index 00000000000..686cbefb5db --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_capital_income_state_tax_invariants.py @@ -0,0 +1,315 @@ +"""Property tests: a dependent's capital income stays off the filers' North +Dakota and Vermont returns. + +Federal adjusted gross income leaves out a tax unit dependent's income +(irs_gross_income), which the dependent reports on their own return (2025 +Form 8814, line 4; the model does not implement the Form 8814 election). Both +states start from the filers' federal return: + +- N.D.C.C. 57-38-30.3(2) defines North Dakota taxable income as "the federal + taxable income of an individual", reduced by 40% of "the taxpayer's net + long-term capital gain" ((d)(1); Form ND-1 line 6, from the filer's + Schedule D lines 15 and 16, "If zero or less ... no exclusion is allowed") + and of qualified dividends ((d)(2); Form ND-1 line 13, from Form 1040 line + 3a), and by interest on U.S. obligations ((a)). +- 32 V.S.A. 5811(21) defines Vermont taxable income as the individual's + federal adjusted gross income, increased by interest on non-Vermont state + and local obligations ((A)(i)) and decreased, "to the extent such income is + included in federal adjusted gross income", by interest on U.S. government + obligations ((B)(i)) and by 40% of gains on "assets held by the taxpayer for + more than three years" ((B)(ii)). + +So for every tax unit in either state, and any capital income its dependents +have (qualified and other dividends, long- and short-term gains and losses, +capital gain distributions, taxable, tax-exempt and U.S. government interest, +and Vermont-eligible long-term gains): + +1. nd_income_tax and vt_income_tax are the same as when the dependents have + none. So are federal adjusted gross income and taxable income, the base + both states start from. +2. nd_qdiv_subtraction is 40% of the filers' qualified dividends, and + nd_ltcg_subtraction is 40% of max(0, min(line 15, line 16)) of the filers' + own Schedule D, with capital gain distributions on line 15. +3. vt_percentage_capital_gains_exclusion is 40% of the filers' eligible gains, + up to the cap, vt_additions is the filers' tax-exempt interest, and + us_govt_interest is the filers' U.S. government interest. + +Two restrictions keep effects that are outside this check out of the draws: + +- The filers earn at least $80,000, so no earned income tax credit is at + stake. Vermont's is a share of the federal credit, whose investment income + test (eitc_relevant_investment_income) is not changed here. +- Vermont filers have at least $50,000 of their own long-term gain and no + short-term loss, and Vermont dependents have gains, not losses. Until + PolicyEngine/policyengine-us#9855 is merged, the $5,000 flat part of + vt_capital_gains_exclusion reads the federal adjusted net capital gain, + which on main still counts a dependent's gains and losses. With these + amounts the flat part is at its cap either way. + +Amounts are whole dollars of at most $400,000, so every sum is exact in single +precision and twins are compared to the cent. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2025 +TOLERANCE = 0.01 + +# Each person's capital income inputs. +CAPITAL_INPUTS = [ + "qualified_dividend_income", + "non_qualified_dividend_income", + "long_term_capital_gains", + "short_term_capital_gains", + "non_sch_d_capital_gains", + "taxable_interest_income", + "us_govt_interest_person", + "tax_exempt_interest_income", + "long_term_capital_gains_on_assets_eligible_for_vt_exclusion", +] +SAME_AS_WITHOUT_DEPENDENT_INCOME = [ + "adjusted_gross_income", + "taxable_income", + "nd_income_tax", + "vt_income_tax", +] +UNIT_OUTPUTS = SAME_AS_WITHOUT_DEPENDENT_INCOME + [ + "nd_qdiv_subtraction", + "nd_ltcg_subtraction", + "vt_percentage_capital_gains_exclusion", + "vt_additions", + "us_govt_interest", +] + + +def amount(low, high): + return st.integers(low, high).map(float) + + +@st.composite +def capital_income(draw, vermont, filer): + """One person's capital income. U.S. government interest is part of + taxable interest, and Vermont-eligible gains are part of long-term + gains.""" + if vermont and filer: + long_term = draw(amount(50_000, 300_000)) + short_term = draw(amount(0, 20_000)) + elif vermont: + long_term = draw(amount(0, 300_000)) + short_term = draw(amount(0, 50_000)) + else: + long_term = draw(amount(-30_000, 300_000)) + short_term = draw(amount(-20_000, 50_000)) + taxable_interest = draw(amount(0, 50_000)) + return { + "qualified_dividend_income": draw(amount(0, 50_000)), + "non_qualified_dividend_income": draw(amount(0, 20_000)), + "long_term_capital_gains": long_term, + "short_term_capital_gains": short_term, + "non_sch_d_capital_gains": draw(amount(0, 20_000)), + "taxable_interest_income": taxable_interest, + "us_govt_interest_person": draw(amount(0, int(taxable_interest))), + "tax_exempt_interest_income": draw(amount(0, 50_000)), + "long_term_capital_gains_on_assets_eligible_for_vt_exclusion": ( + draw(amount(0, int(long_term))) if long_term > 0 else 0.0 + ), + } + + +@st.composite +def tax_units(draw): + state = draw(st.sampled_from(["ND", "VT"])) + vermont = state == "VT" + n_filers = draw(st.integers(1, 2)) + filers = [ + { + "age": draw(st.integers(30, 64)), + "employment_income": draw(amount(80_000, 400_000)) if i == 0 else 0.0, + **draw(capital_income(vermont, filer=True)), + } + for i in range(n_filers) + ] + dependents = [ + { + "age": draw(st.integers(1, 17)), + **draw(capital_income(vermont, filer=False)), + } + for _ in range(draw(st.integers(1, 2))) + ] + return {"state": state, "filers": filers, "dependents": dependents} + + +def without_dependent_income(unit): + zero = {name: 0.0 for name in CAPITAL_INPUTS} + return {**unit, "dependents": [{**d, **zero} for d in unit["dependents"]]} + + +def build_situation(units): + """One tax unit per household: the head, an optional spouse and the + dependents, each dependent in a marital unit of their own.""" + people, tax_units_out, marital_units, households = {}, {}, {}, {} + for i, unit in enumerate(units): + members = [] + for j, person in enumerate(unit["filers"] + unit["dependents"]): + name = f"person_{i}_{j}" + filer = j < len(unit["filers"]) + people[name] = { + "is_tax_unit_head": {YEAR: j == 0}, + "is_tax_unit_spouse": {YEAR: j == 1 and filer}, + "is_tax_unit_dependent": {YEAR: not filer}, + **{key: {YEAR: value} for key, value in person.items()}, + } + members.append(name) + if not filer: + marital_units[f"dependent_{i}_{j}"] = {"members": [name]} + tax_units_out[f"tax_unit_{i}"] = {"members": members} + marital_units[f"filers_{i}"] = {"members": members[: len(unit["filers"])]} + households[f"household_{i}"] = { + "members": members, + "state_code": {YEAR: unit["state"]}, + } + return { + "people": people, + "tax_units": tax_units_out, + "marital_units": marital_units, + "households": households, + } + + +def calculate(units): + simulation = Simulation(situation=build_situation(units)) + results = { + variable: np.asarray(simulation.calculate(variable, YEAR)) + for variable in UNIT_OUTPUTS + } + p = simulation.tax_benefit_system.parameters(YEAR).gov.states + return results, p + + +def filer_total(unit, name): + return sum(person[name] for person in unit["filers"]) + + +def check(units): + twins = [without_dependent_income(unit) for unit in units] + results, p = calculate(units + twins) + n = len(units) + + # 1. The same taxes and federal base as without the dependents' income. + for variable in SAME_AS_WITHOUT_DEPENDENT_INCOME: + np.testing.assert_allclose( + results[variable][:n], + results[variable][n:], + atol=TOLERANCE, + err_msg=variable, + ) + + nd = p.nd.tax.income.taxable_income.subtractions + vt = p.vt.tax.income.agi.exclusions.capital_gain.percentage + is_nd = np.array([unit["state"] == "ND" for unit in units]) + is_vt = ~is_nd + long_term = np.array( + [ + filer_total(u, "long_term_capital_gains") + + filer_total(u, "non_sch_d_capital_gains") + for u in units + ] + ) + short_term = np.array([filer_total(u, "short_term_capital_gains") for u in units]) + reference = { + # 2. North Dakota, from the filers' own amounts. + "nd_qdiv_subtraction": nd.qdiv_fraction + * np.array([filer_total(u, "qualified_dividend_income") for u in units]), + "nd_ltcg_subtraction": nd.ltcg_fraction + * np.maximum(0, np.minimum(long_term, long_term + short_term)), + # 3. Vermont, from the filers' own amounts. + "vt_percentage_capital_gains_exclusion": np.minimum( + vt.rate + * np.array( + [ + filer_total( + u, "long_term_capital_gains_on_assets_eligible_for_vt_exclusion" + ) + for u in units + ] + ), + vt.cap, + ), + "vt_additions": np.array( + [filer_total(u, "tax_exempt_interest_income") for u in units] + ), + } + for variable, expected in reference.items(): + in_state = is_nd if variable.startswith("nd_") else is_vt + np.testing.assert_allclose( + results[variable][:n][in_state], + expected[in_state], + atol=TOLERANCE, + err_msg=variable, + ) + np.testing.assert_allclose( + results["us_govt_interest"][:n], + [filer_total(u, "us_govt_interest_person") for u in units], + atol=TOLERANCE, + ) + + +# A simulation's cost is mostly fixed, so each example is a batch of tax +# units and there are few examples. +@settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(tax_units(), min_size=10, max_size=30)) +def test_dependent_capital_income_stays_off_nd_and_vt_returns(units): + check(units) + + +def test_reported_examples(): + """The two households that showed the leak (TY2025): in North Dakota a + head with $50,000 of wages and $1,000 of qualified dividends, and in + Vermont a head with $150,000 of wages and a $10,000 long-term gain, each + with a dependent child who has dividends or a Vermont-eligible gain.""" + zero = {name: 0.0 for name in CAPITAL_INPUTS} + nd = { + "state": "ND", + "filers": [ + { + "age": 40, + "employment_income": 50_000.0, + **zero, + "qualified_dividend_income": 1_000.0, + } + ], + "dependents": [{"age": 10, **zero, "qualified_dividend_income": 5_000.0}], + } + vt = { + "state": "VT", + "filers": [ + { + "age": 40, + "employment_income": 150_000.0, + **zero, + "long_term_capital_gains": 10_000.0, + } + ], + "dependents": [ + { + "age": 10, + **zero, + "long_term_capital_gains": 100_000.0, + "long_term_capital_gains_on_assets_eligible_for_vt_exclusion": 100_000.0, + } + ], + } + check([nd, vt]) + results, _ = calculate([nd, vt]) + assert results["nd_qdiv_subtraction"][0] == 400 + assert results["vt_percentage_capital_gains_exclusion"][1] == 0 + np.testing.assert_allclose(results["vt_income_tax"][1], 6_623.20, atol=TOLERANCE) diff --git a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.yaml index e84729d129e..273d46e0802 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.yaml @@ -102,3 +102,37 @@ output: # Net capital gains = 40,000 (gain), so 25% of $50K LTCG = $12,500 az_long_term_capital_gains_subtraction: 12_500 + +- name: A dependent's long-term gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 10_000 + child: + age: 10 + long_term_capital_gains: 100_000 + household: + state_code: AZ + output: + az_long_term_capital_gains_subtraction: 2_500 + +- name: A dependent's short-term loss does not take away the filer's subtraction + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 10_000 + child: + age: 10 + short_term_capital_gains: -50_000 + household: + state_code: AZ + output: + az_long_term_capital_gains_subtraction: 2_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml new file mode 100644 index 00000000000..2da5757b53a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml @@ -0,0 +1,18 @@ +- name: A dependent's U.S. government interest is not subtracted + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + us_govt_interest_person: 200 + taxable_interest_income: 200 + child: + age: 10 + us_govt_interest_person: 50_000 + taxable_interest_income: 50_000 + household: + state_code: DC + output: + dc_income_subtractions: [200, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml new file mode 100644 index 00000000000..4ec19202c05 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml @@ -0,0 +1,18 @@ +- name: A dependent's U.S. government interest is not subtracted + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + us_govt_interest_person: 200 + taxable_interest_income: 200 + child: + age: 10 + us_govt_interest_person: 50_000 + taxable_interest_income: 50_000 + household: + state_code: DE + output: + de_subtractions: [200, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_reportable_social_security.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_reportable_social_security.yaml index d0b89d39608..520300edea5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_reportable_social_security.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_reportable_social_security.yaml @@ -49,3 +49,37 @@ state_code: IA output: ia_reportable_social_security: 1_500 # one-half of gross social security + +- name: A dependent's tax-exempt interest and benefits are on the dependent's own return + period: 2022 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + tax_exempt_interest_income: 50_000 + social_security_survivors: 10_000 + household: + state_code: IA + output: + ia_reportable_social_security: 0 + +- name: Same household without the dependent's income + period: 2022 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + household: + state_code: IA + output: + ia_reportable_social_security: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/il/tax/income/income/il_base_income_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/il/tax/income/income/il_base_income_additions.yaml new file mode 100644 index 00000000000..7fd38eec3db --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/il/tax/income/income/il_base_income_additions.yaml @@ -0,0 +1,16 @@ +- name: A dependent's tax-exempt interest is not added + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + tax_exempt_interest_income: 300 + child: + age: 10 + tax_exempt_interest_income: 5_000 + household: + state_code: IL + output: + il_base_income_additions: 300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.yaml new file mode 100644 index 00000000000..a5b5a73c38f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.yaml @@ -0,0 +1,20 @@ +- name: A dependent's dividends and short-term gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + qualified_dividend_income: 1_000 + child: + age: 10 + qualified_dividend_income: 5_000 + short_term_capital_gains: 50_000 + household: + state_code: MA + output: + ma_part_a_gross_income: 1_000 + ma_part_a_agi: 1_000 + ma_part_b_gross_income: 60_000 + ma_part_a_taxable_dividend_income: 1_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.yaml new file mode 100644 index 00000000000..c520cb07b13 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.yaml @@ -0,0 +1,36 @@ +- name: A dependent's long-term gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 2_000 + child: + age: 10 + long_term_capital_gains: 300_000 + household: + state_code: MA + output: + ma_part_c_gross_income: 2_000 + ma_part_b_gross_income: 60_000 + ma_income_tax: 2_170 + +- name: Same household without the dependent's gains + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 2_000 + child: + age: 10 + household: + state_code: MA + output: + ma_part_c_gross_income: 2_000 + ma_part_b_gross_income: 60_000 + ma_income_tax: 2_170 diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.yaml index 1e12d9fed73..68a00a71e8f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.yaml @@ -244,3 +244,20 @@ # Net capital gains: $300,000 (should potentially be less with exclusions) # Surtax: $300,000 * 2% = $6,000 (may overstate actual liability) md_capital_gains_surtax: 6_000 + +- name: A dependent's gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 400_000 + long_term_capital_gains: 10_000 + child: + age: 10 + long_term_capital_gains: 300_000 + household: + state_code: MD + output: + md_capital_gains_surtax: 200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_and_property_tax_fairness_credit_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_and_property_tax_fairness_credit_income.yaml new file mode 100644 index 00000000000..21c8b920218 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_and_property_tax_fairness_credit_income.yaml @@ -0,0 +1,34 @@ +- name: A dependent's tax-exempt interest is on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 82 + social_security_retirement: 18_000 + taxable_pension_income: 8_000 + tax_exempt_interest_income: 500 + child: + age: 10 + tax_exempt_interest_income: 50_000 + household: + state_code: ME + output: + me_sales_and_property_tax_fairness_credit_income: 26_500 + +- name: Same household without the dependent's interest + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 82 + social_security_retirement: 18_000 + taxable_pension_income: 8_000 + tax_exempt_interest_income: 500 + child: + age: 10 + household: + state_code: ME + output: + me_sales_and_property_tax_fairness_credit_income: 26_500 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 59c53198711..3ff6750194d 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 @@ -94,3 +94,37 @@ state_code: MN output: mn_social_security_subtraction: 25_500 + +- name: A dependent's tax-exempt interest and benefits are on the dependent's own return + period: 2022 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + tax_exempt_interest_income: 50_000 + social_security_survivors: 10_000 + household: + state_code: MN + output: + mn_social_security_subtraction: 4_260 + +- name: Same household without the dependent's income + period: 2022 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + household: + state_code: MN + output: + mn_social_security_subtraction: 4_260 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml index 72ea9ba6c38..b18389de370 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml @@ -123,3 +123,42 @@ state_code: MT output: mt_capital_gains_tax_joint: 409 + +- name: A dependent's gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 2_000 + child: + age: 10 + long_term_capital_gains: 300_000 + household: + state_code: MT + output: + mt_capital_gains_tax_joint: 82 + mt_regular_income_tax_joint: 1_765.72 + mt_capital_gains_tax_applicable_threshold_joint: 0 + mt_income_tax: 1_847.72 + +- name: Same household without the dependent's gains + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 2_000 + child: + age: 10 + household: + state_code: MT + output: + mt_capital_gains_tax_joint: 82 + mt_regular_income_tax_joint: 1_765.72 + mt_capital_gains_tax_applicable_threshold_joint: 0 + mt_income_tax: 1_847.72 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/mt_capital_gain_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/mt_capital_gain_credit.yaml index 370c349f925..2f572d9e4cd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/mt_capital_gain_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/mt_capital_gain_credit.yaml @@ -13,3 +13,20 @@ state_code: MT output: mt_capital_gain_credit: 60 + +- name: A dependent's gains give no credit on this return + period: 2023 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 2_000 + child: + age: 10 + long_term_capital_gains: 50_000 + household: + state_code: MT + output: + mt_capital_gain_credit: [40, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/exemptions/mt_interest_exemption_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/exemptions/mt_interest_exemption_person.yaml index c1ec3ef42bb..8578bfaae21 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/exemptions/mt_interest_exemption_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/exemptions/mt_interest_exemption_person.yaml @@ -58,3 +58,21 @@ output: mt_interest_exemption_person: [0, 0] + +- name: A dependent's interest does not dilute the filer's share + period: 2023 + input: + people: + head: + is_tax_unit_head: true + age: 70 + taxable_interest_income: 2_000 + social_security_retirement: 20_000 + child: + age: 10 + taxable_interest_income: 50_000 + household: + state_code: MT + output: + mt_interest_exemption_person: [800, 0] + mt_interest_exemption: 800 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_joint.yaml new file mode 100644 index 00000000000..b23971c2bcd --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_joint.yaml @@ -0,0 +1,18 @@ +- name: A dependent's U.S. government interest is not subtracted + period: 2023 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + us_govt_interest_person: 200 + taxable_interest_income: 200 + child: + age: 10 + us_govt_interest_person: 50_000 + taxable_interest_income: 50_000 + household: + state_code: MT + output: + mt_agi_joint: 60_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_taxable_social_security_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_taxable_social_security_joint.yaml new file mode 100644 index 00000000000..3d457ec24e8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_taxable_social_security_joint.yaml @@ -0,0 +1,35 @@ +- name: A dependent's tax-exempt interest and benefits are on the dependent's own return + period: 2023 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + tax_exempt_interest_income: 50_000 + social_security_survivors: 10_000 + household: + state_code: MT + output: + mt_taxable_social_security_joint: 5_350 + taxable_social_security: [5_350, 0] + +- name: Same household without the dependent's income + period: 2023 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + household: + state_code: MT + output: + mt_taxable_social_security_joint: 5_350 + taxable_social_security: [5_350, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_ltcg_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_ltcg_subtraction.yaml index d3a64990c9d..9daffc996be 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_ltcg_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_ltcg_subtraction.yaml @@ -31,3 +31,68 @@ state_code: ND output: nd_ltcg_subtraction: (7_000 - 2_000) * 0.40 + +- name: A dependent's long-term capital gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 50_000 + long_term_capital_gains: 2_000 + child: + age: 10 + long_term_capital_gains: 10_000 + household: + state_code: ND + output: + nd_ltcg_subtraction: 800 + +- name: A dependent's short-term capital loss does not reduce the filer's subtraction + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 50_000 + long_term_capital_gains: 2_000 + child: + age: 10 + short_term_capital_gains: -1_500 + household: + state_code: ND + output: + nd_ltcg_subtraction: 800 + +- name: No exclusion when the filers have a net long-term capital loss + # Form ND-1 line 6 worksheet, line 1: if zero or less, no exclusion. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 50_000 + long_term_capital_gains: -2_000 + short_term_capital_gains: 5_000 + household: + state_code: ND + output: + nd_ltcg_subtraction: 0 + +- name: Capital gain distributions reported without Schedule D count + # Form ND-1 line 6 worksheet: enter the distributions on line 3. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 50_000 + non_sch_d_capital_gains: 1_000 + household: + state_code: ND + output: + nd_ltcg_subtraction: 400 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_qdiv_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_qdiv_subtraction.yaml index 0680681e7d2..7d7150b1ea1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_qdiv_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_qdiv_subtraction.yaml @@ -12,3 +12,20 @@ state_code: ND output: nd_qdiv_subtraction: 800 * 0.40 + +- name: A dependent's qualified dividends are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 50_000 + qualified_dividend_income: 1_000 + child: + age: 10 + qualified_dividend_income: 5_000 + household: + state_code: ND + output: + nd_qdiv_subtraction: 400 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/tax/income/nh_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/tax/income/nh_taxable_income.yaml new file mode 100644 index 00000000000..285c751a8cb --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/tax/income/nh_taxable_income.yaml @@ -0,0 +1,18 @@ +- name: A dependent's interest and dividends are on the dependent's own return + period: 2024 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + taxable_interest_income: 3_000 + child: + age: 10 + taxable_interest_income: 50_000 + qualified_dividend_income: 5_000 + household: + state_code: NH + output: + nh_taxable_income: 600 + nh_total_exemptions: 2_400 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml new file mode 100644 index 00000000000..d0ea8b2f9c3 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml @@ -0,0 +1,18 @@ +- name: A dependent's U.S. government interest is not deducted + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + us_govt_interest_person: 200 + taxable_interest_income: 200 + child: + age: 10 + us_govt_interest_person: 50_000 + taxable_interest_income: 50_000 + household: + state_code: OH + output: + oh_deductions: [200, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.yaml new file mode 100644 index 00000000000..650bbb038ff --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.yaml @@ -0,0 +1,34 @@ +- name: A dependent's tax-exempt interest is not the claimant's eligibility income + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 82 + social_security_retirement: 18_000 + taxable_pension_income: 8_000 + tax_exempt_interest_income: 500 + child: + age: 10 + tax_exempt_interest_income: 50_000 + household: + state_code: PA + output: + pa_eligibility_income: 500 + +- name: Same household without the dependent's interest + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 82 + social_security_retirement: 18_000 + taxable_pension_income: 8_000 + tax_exempt_interest_income: 500 + child: + age: 10 + household: + state_code: PA + output: + pa_eligibility_income: 500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tax/income/deductions/net_captial_gain/sc_net_capital_gain_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tax/income/deductions/net_captial_gain/sc_net_capital_gain_deduction.yaml index d2906f41389..0be4cc161c1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tax/income/deductions/net_captial_gain/sc_net_capital_gain_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tax/income/deductions/net_captial_gain/sc_net_capital_gain_deduction.yaml @@ -33,3 +33,20 @@ short_term_capital_gains: -5_000 output: sc_net_capital_gain_deduction: 880 + +- name: A dependent's gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 10_000 + child: + age: 10 + long_term_capital_gains: 100_000 + household: + state_code: SC + output: + sc_net_capital_gain_deduction: 4_400 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml index 71275114c66..0812b78f10f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml @@ -138,3 +138,20 @@ state_code: UT output: ut_retirement_credit_max: 300 + +- name: An elderly dependent is not a claimant, and a dependent's interest is on their own return + period: 2022 + input: + people: + head: + is_tax_unit_head: true + age: 75 + taxable_pension_income: 20_000 + parent: + age: 95 + is_tax_unit_dependent: true + tax_exempt_interest_income: 50_000 + household: + state_code: UT + output: + ut_retirement_credit_max: 450 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml index 9f8b8f14077..875e5d85c1c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml @@ -52,3 +52,36 @@ state_code: UT output: ut_ss_benefits_credit_max: 4_700 + +- name: A dependent's tax-exempt interest is on the dependent's own return + period: 2022 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + tax_exempt_interest_income: 50_000 + household: + state_code: UT + output: + ut_ss_benefits_credit_max: 259.48 + +- name: Same household without the dependent's interest + period: 2022 + input: + people: + head: + is_tax_unit_head: true + age: 70 + social_security_retirement: 30_000 + taxable_pension_income: 20_000 + child: + age: 10 + household: + state_code: UT + output: + ut_ss_benefits_credit_max: 259.48 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_ctc_potential.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_ctc_potential.yaml new file mode 100644 index 00000000000..64cf55c3429 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_ctc_potential.yaml @@ -0,0 +1,15 @@ +- name: A dependent's tax-exempt interest is on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 40_000 + child: + age: 3 + tax_exempt_interest_income: 50_000 + household: + state_code: UT + output: + ut_ctc_potential: 1_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions.yaml new file mode 100644 index 00000000000..3adb2787ed9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions.yaml @@ -0,0 +1,19 @@ +- name: A dependent's U.S. government interest is not subtracted + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + us_govt_interest_person: 1_000 + taxable_interest_income: 1_000 + child: + age: 10 + us_govt_interest_person: 5_000 + taxable_interest_income: 5_000 + household: + state_code: VA + output: + va_subtractions: 1_000 + va_subtractions_person: [1_000, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_additions.yaml new file mode 100644 index 00000000000..6ed0e14411f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_additions.yaml @@ -0,0 +1,16 @@ +- name: A dependent's tax-exempt interest is not added + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + tax_exempt_interest_income: 300 + child: + age: 10 + tax_exempt_interest_income: 5_000 + household: + state_code: VT + output: + vt_additions: 300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_percentage_capital_gains_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_percentage_capital_gains_exclusion.yaml index bd5b9f62e60..c2677e7fcb0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_percentage_capital_gains_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_percentage_capital_gains_exclusion.yaml @@ -43,3 +43,43 @@ long_term_capital_gains_on_assets_eligible_for_vt_exclusion: 10_000 output: vt_percentage_capital_gains_exclusion: 4_000 # 40% of 10k eligible only + +- name: A dependent's eligible gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 150_000 + long_term_capital_gains: 10_000 + child: + age: 10 + long_term_capital_gains: 100_000 + long_term_capital_gains_on_assets_eligible_for_vt_exclusion: 100_000 + household: + state_code: VT + output: + vt_percentage_capital_gains_exclusion: 0 + vt_capital_gains_exclusion: 5_000 + adjusted_gross_income: 160_000 + vt_income_tax: 6_623.2 + +- name: Same household without the dependent's gains + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 150_000 + long_term_capital_gains: 10_000 + child: + age: 10 + household: + state_code: VT + output: + vt_percentage_capital_gains_exclusion: 0 + vt_capital_gains_exclusion: 5_000 + adjusted_gross_income: 160_000 + vt_income_tax: 6_623.2 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/tax/income/wa_capital_gains_tax.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/tax/income/wa_capital_gains_tax.yaml index f9488394320..d351e6532df 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/tax/income/wa_capital_gains_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/tax/income/wa_capital_gains_tax.yaml @@ -93,3 +93,20 @@ # Taxable LTCG: $2,000,000 # 7% x $2,000,000 + 2.9% x $1,000,000 = $169,000 wa_capital_gains_tax: 169_000 + +- name: A dependent's gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 100_000 + child: + age: 10 + long_term_capital_gains: 300_000 + household: + state_code: WA + output: + wa_capital_gains_tax: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_additions.yaml new file mode 100644 index 00000000000..10e3aca4f23 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_additions.yaml @@ -0,0 +1,16 @@ +- name: A dependent's tax-exempt interest is not added + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + tax_exempt_interest_income: 300 + child: + age: 10 + tax_exempt_interest_income: 5_000 + household: + state_code: WI + output: + wi_additions: 300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_gain_loss_subtration.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_gain_loss_subtration.yaml index 11635a6391e..70b6e6f1a7f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_gain_loss_subtration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_gain_loss_subtration.yaml @@ -27,3 +27,20 @@ state_code: WI output: wi_capital_gain_loss_subtraction: 0 + +- name: A dependent's gains are on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 10_000 + child: + age: 10 + long_term_capital_gains: 100_000 + household: + state_code: WI + output: + wi_capital_gain_loss_subtraction: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_loss.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_loss.yaml index 08d9dd4d723..4b2cb6bc686 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_loss.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_capital_loss.yaml @@ -39,3 +39,20 @@ filing_status: JOINT output: wi_capital_loss: 2_000 + +- name: A dependent's capital loss is on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + long_term_capital_gains: 1_000 + child: + age: 10 + short_term_capital_gains: -20_000 + household: + state_code: WI + output: + wi_capital_loss: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.yaml new file mode 100644 index 00000000000..67f5c43b829 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.yaml @@ -0,0 +1,17 @@ +- name: A dependent's tax-exempt interest is on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 82 + social_security_retirement: 18_000 + taxable_pension_income: 8_000 + tax_exempt_interest_income: 500 + child: + age: 10 + tax_exempt_interest_income: 50_000 + household: + state_code: WV + output: + wv_low_income_family_tax_credit_agi: 8_500 diff --git a/policyengine_us/tests/policy/baseline/household/income/tax_unit/us_govt_interest.yaml b/policyengine_us/tests/policy/baseline/household/income/tax_unit/us_govt_interest.yaml new file mode 100644 index 00000000000..d74085f3597 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/income/tax_unit/us_govt_interest.yaml @@ -0,0 +1,18 @@ +- name: A dependent's U.S. government interest is on the dependent's own return + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + us_govt_interest_person: 1_000 + taxable_interest_income: 1_000 + child: + age: 10 + us_govt_interest_person: 5_000 + taxable_interest_income: 5_000 + household: + state_code: CA + output: + us_govt_interest: 1_000 diff --git a/policyengine_us/variables/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.py b/policyengine_us/variables/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.py index 4817bc865b6..d8f51eb3b53 100644 --- a/policyengine_us/variables/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.py +++ b/policyengine_us/variables/gov/states/az/tax/income/subtractions/capital_gains/az_long_term_capital_gains_subtraction.py @@ -19,8 +19,17 @@ def formula(tax_unit, period, parameters): # Per ARS 43-1022(22): subtraction is for "net long-term capital gain # included in federal adjusted gross income". When there's an overall # capital loss, no LTCG is included in federal AGI. - long_term_capital_gains = add(tax_unit, period, ["long_term_capital_gains"]) - net_capital_gains = tax_unit("net_capital_gains", period) + # Federal adjusted gross income leaves out a tax unit dependent's gains, + # which are on the dependent's own return, so only the head's and + # spouse's gains count. + long_term_capital_gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) + net_capital_gains = tax_unit_non_dep_add( + tax_unit, + period, + ["long_term_capital_gains", "short_term_capital_gains"], + ) # Only include LTCG when there's a net capital gain overall ltcg_in_agi = where( diff --git a/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py b/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py index a8bccb24d81..cc41f847bf5 100644 --- a/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py +++ b/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py @@ -16,5 +16,14 @@ class dc_income_subtractions(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.dc.tax.income.subtractions total_subtractions = add(person, period, p.sources) + # A tax unit dependent's U.S. government interest is on the dependent's + # own return and never in the filer's federal AGI, so it is not + # subtracted here, where each person's amounts are summed into the + # filer's. + if "us_govt_interest_person" in p.sources: + dependent = person("is_tax_unit_dependent", period) + total_subtractions = total_subtractions - dependent * person( + "us_govt_interest_person", period + ) # Prevent negative subtractions from acting as additions return max_(0, total_subtractions) diff --git a/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py b/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py index 708fdab8acb..a4a8b2cf107 100644 --- a/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py +++ b/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py @@ -12,5 +12,14 @@ class de_subtractions(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.de.tax.income.subtractions total_subtractions = add(person, period, p.subtractions) + # A tax unit dependent's U.S. government interest is on the dependent's + # own return and never in the filer's federal AGI, so it is not + # subtracted here, where each person's amounts are summed into the + # filer's. + if "us_govt_interest_person" in p.subtractions: + dependent = person("is_tax_unit_dependent", period) + total_subtractions = total_subtractions - dependent * person( + "us_govt_interest_person", period + ) # Prevent negative subtractions from acting as additions return max_(0, total_subtractions) diff --git a/policyengine_us/variables/gov/states/ia/tax/income/ia_reportable_social_security.py b/policyengine_us/variables/gov/states/ia/tax/income/ia_reportable_social_security.py index 8964b2b87f6..af7410b3698 100644 --- a/policyengine_us/variables/gov/states/ia/tax/income/ia_reportable_social_security.py +++ b/policyengine_us/variables/gov/states/ia/tax/income/ia_reportable_social_security.py @@ -14,11 +14,14 @@ class ia_reportable_social_security(Variable): defined_for = StateCode.IA def formula(tax_unit, period, parameters): - benefits = add(tax_unit, period, ["social_security"]) + # The worksheet uses the head's and spouse's amounts from their federal + # return; a tax unit dependent's benefits and interest are on the + # dependent's own return. + benefits = tax_unit_non_dep_add(tax_unit, period, ["social_security"]) income = ( tax_unit("adjusted_gross_income", period) - add(tax_unit, period, ["taxable_social_security"]) - + add(tax_unit, period, ["tax_exempt_interest_income"]) + + tax_unit_non_dep_add(tax_unit, period, ["tax_exempt_interest_income"]) ) p = parameters(period).gov.states.ia.tax.income filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/states/il/tax/income/base_income/il_base_income_additions.py b/policyengine_us/variables/gov/states/il/tax/income/base_income/il_base_income_additions.py index b2e461a9c0e..2f2e47ebb02 100644 --- a/policyengine_us/variables/gov/states/il/tax/income/base_income/il_base_income_additions.py +++ b/policyengine_us/variables/gov/states/il/tax/income/base_income/il_base_income_additions.py @@ -9,4 +9,10 @@ class il_base_income_additions(Variable): definition_period = YEAR defined_for = StateCode.IL - adds = "gov.states.il.tax.income.base.additions" + def formula(tax_unit, period, parameters): + # Illinois adds these items to federal adjusted gross income, which + # leaves out a tax unit dependent's income (the dependent files their + # own return), so only the head's and spouse's amounts count, such as + # their federally tax-exempt interest (IL-1040 line 2). + p = parameters(period).gov.states.il.tax.income.base + return tax_unit_non_dep_add(tax_unit, period, p.additions) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py b/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py index 2120a051cec..e4fd4ca15bb 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py @@ -12,10 +12,15 @@ class ma_part_a_agi(Variable): def formula(tax_unit, period, parameters): part_a_gross_income = tax_unit("ma_part_a_gross_income", period) - short_term_capital_gains = add(tax_unit, period, ["short_term_capital_gains"]) + # Part B gross income starts from irs_gross_income, which leaves out + # tax unit dependents, whose interest, dividends and gains are on their + # own returns, so Parts A and C count only the head's and spouse's. + short_term_capital_gains = tax_unit_non_dep_add( + tax_unit, period, ["short_term_capital_gains"] + ) short_term_capital_loss = max_(0, -short_term_capital_gains) nonnegative_short_term_capital_gains = max_(0, short_term_capital_gains) - interest_and_dividends = add( + interest_and_dividends = tax_unit_non_dep_add( tax_unit, period, ["taxable_interest_income", "dividend_income"] ) @@ -30,7 +35,9 @@ def formula(tax_unit, period, parameters): short_term_capital_loss, ), ) - long_term_capital_gains = add(tax_unit, period, ["long_term_capital_gains"]) + long_term_capital_gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) long_term_capital_loss = max_(0, -long_term_capital_gains) long_term_loss_against_short_term_gain = min_( @@ -55,7 +62,7 @@ def formula(tax_unit, period, parameters): ), ) - long_term_capital_gains_on_collectibles = add( + long_term_capital_gains_on_collectibles = tax_unit_non_dep_add( tax_unit, period, ["long_term_capital_gains_on_collectibles"] ) nonnegative_long_term_capital_gains_on_collectibles = max_( diff --git a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.py b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.py index 9bfa54ab311..4d873c8afcf 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_a_gross_income.py @@ -11,8 +11,11 @@ class ma_part_a_gross_income(Variable): defined_for = StateCode.MA def formula(tax_unit, period, parameters): - dividends = add(tax_unit, period, ["dividend_income"]) + # Part B gross income starts from irs_gross_income, which leaves out + # tax unit dependents, whose interest, dividends and gains are on their + # own returns, so Parts A and C count only the head's and spouse's. + dividends = tax_unit_non_dep_add(tax_unit, period, ["dividend_income"]) nonnegative_short_term_capital_gains = max_( - 0, add(tax_unit, period, ["short_term_capital_gains"]) + 0, tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) ) return dividends + nonnegative_short_term_capital_gains diff --git a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py index 9536895d297..5434d68bf78 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py @@ -11,10 +11,13 @@ class ma_part_c_gross_income(Variable): defined_for = StateCode.MA def formula(tax_unit, period, parameters): + # Part B gross income starts from irs_gross_income, which leaves out + # tax unit dependents, whose interest, dividends and gains are on their + # own returns, so Parts A and C count only the head's and spouse's. # Long-term capital gains - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) + ltcg = tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) # Short-term capital gains (can be negative) - stcg = add(tax_unit, period, ["short_term_capital_gains"]) + stcg = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) # Per MA Schedule B line 22, short-term losses offset long-term gains stcg_losses = min_(0, stcg) return max_(0, ltcg + stcg_losses) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_cg_excess_exemption.py b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_cg_excess_exemption.py index 66b8ebc6a89..0d00eb6356a 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_cg_excess_exemption.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_cg_excess_exemption.py @@ -12,7 +12,8 @@ class ma_part_a_cg_excess_exemption(Variable): def formula(tax_unit, period, parameters): part_a_agi = tax_unit("ma_part_a_agi", period) - dividends = add(tax_unit, period, ["dividend_income"]) + # The head's and spouse's dividends, as in ma_part_a_gross_income. + dividends = tax_unit_non_dep_add(tax_unit, period, ["dividend_income"]) stcg_agi = part_a_agi - dividends div_excess_exemption = tax_unit("ma_part_a_div_excess_exemption", period) return max_(0, div_excess_exemption - stcg_agi) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_div_excess_exemption.py b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_div_excess_exemption.py index e2f70b2a180..a90468203e3 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_div_excess_exemption.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_div_excess_exemption.py @@ -11,6 +11,7 @@ class ma_part_a_div_excess_exemption(Variable): defined_for = StateCode.MA def formula(tax_unit, period, parameters): - dividends = add(tax_unit, period, ["dividend_income"]) + # The head's and spouse's dividends, as in ma_part_a_gross_income. + dividends = tax_unit_non_dep_add(tax_unit, period, ["dividend_income"]) partb_excess_exemption = tax_unit("ma_part_b_excess_exemption", period) return max_(0, partb_excess_exemption - dividends) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_capital_gains_income.py b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_capital_gains_income.py index 6a172d727ad..6bf4d235c18 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_capital_gains_income.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_capital_gains_income.py @@ -12,7 +12,8 @@ class ma_part_a_taxable_capital_gains_income(Variable): def formula(tax_unit, period, parameters): part_a_agi = tax_unit("ma_part_a_agi", period) - dividends = add(tax_unit, period, ["dividend_income"]) + # The head's and spouse's dividends, as in ma_part_a_gross_income. + dividends = tax_unit_non_dep_add(tax_unit, period, ["dividend_income"]) stcg_agi = part_a_agi - dividends div_excess_exemption = tax_unit("ma_part_a_div_excess_exemption", period) return max_(0, stcg_agi - div_excess_exemption) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_dividend_income.py b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_dividend_income.py index 3de2a5accfb..dc8372e0000 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_dividend_income.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_a_taxable_dividend_income.py @@ -11,6 +11,7 @@ class ma_part_a_taxable_dividend_income(Variable): defined_for = StateCode.MA def formula(tax_unit, period, parameters): - dividends = add(tax_unit, period, ["dividend_income"]) + # The head's and spouse's dividends, as in ma_part_a_gross_income. + dividends = tax_unit_non_dep_add(tax_unit, period, ["dividend_income"]) part_b_excess_exemption = tax_unit("ma_part_b_excess_exemption", period) return max_(0, dividends - part_b_excess_exemption) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_b_taxable_income_deductions.py b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_b_taxable_income_deductions.py index 2a338d9cfcc..07decd4f399 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_b_taxable_income_deductions.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/taxable_income/ma_part_b_taxable_income_deductions.py @@ -35,7 +35,11 @@ def formula(tax_unit, period, parameters): # Bank interest deduction. filing_status = tax_unit("ma_filing_status", period) if tax.exemptions.interest.in_effect: - bank_interest = add(tax_unit, period, ["taxable_interest_income"]) + # The head's and spouse's interest: a tax unit dependent's is on + # the dependent's own return. + bank_interest = tax_unit_non_dep_add( + tax_unit, period, ["taxable_interest_income"] + ) bank_interest_deduction = min_( tax.exemptions.interest.amount[filing_status], bank_interest, diff --git a/policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.py b/policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.py index 13027bbfbbd..dc5cbc72cad 100644 --- a/policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.py +++ b/policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.py @@ -17,7 +17,9 @@ def formula(tax_unit, period, parameters): # Get net capital gains (sum of long-term and short-term) # NOTE: This is the basic implementation using readily available variables - total_capital_gains = add( + # A tax unit dependent's gains are on the dependent's own return, so + # only the head's and spouse's gains count. + total_capital_gains = tax_unit_non_dep_add( tax_unit, period, ["short_term_capital_gains", "long_term_capital_gains"], diff --git a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/me_sales_and_property_tax_fairness_credit_income.py b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/me_sales_and_property_tax_fairness_credit_income.py index 7511a063e49..d1a62fd9e23 100644 --- a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/me_sales_and_property_tax_fairness_credit_income.py +++ b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/me_sales_and_property_tax_fairness_credit_income.py @@ -8,8 +8,15 @@ class me_sales_and_property_tax_fairness_credit_income(Variable): label = "Maine sales and property tax fairness credit total income" definition_period = YEAR reference = ( - "https://legislature.maine.gov/statutes/36/title36sec5219-KK.html" # 1. D. + "https://legislature.maine.gov/statutes/36/title36sec5219-KK.html", # 1. D. + "https://legislature.maine.gov/statutes/36/title36sec5213-A.html", # 1. B. ) defined_for = StateCode.ME - adds = "gov.states.me.tax.income.credits.fairness.income_sources" + def formula(tax_unit, period, parameters): + # 36 M.R.S. 5213-A(1)(B) and 5219-KK(1)(D): federal adjusted gross + # income increased by interest received to the extent not included in + # it. Federal AGI is the head's and spouse's, so the interest is too: a + # tax unit dependent's is on the dependent's own return. + p = parameters(period).gov.states.me.tax.income.credits.fairness + return tax_unit_non_dep_add(tax_unit, period, p.income_sources) 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..ca3158b101d 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 @@ -41,12 +41,19 @@ def formula(tax_unit, period, parameters): # ... alternative benefit subtraction amount us_gross_income = add(tax_unit, period, ["irs_gross_income"]) adj_income = us_gross_income - us_taxable_oasdi - total_oasdi = add(tax_unit, period, ["social_security"]) + # The worksheet uses the head's and spouse's amounts from their federal + # return; a tax unit dependent's benefits, interest and student loan + # interest are on the dependent's own return. + total_oasdi = tax_unit_non_dep_add(tax_unit, period, ["social_security"]) oasdi_amount = total_oasdi * total_benefit_fraction - tax_exempt_int = add(tax_unit, period, ["tax_exempt_interest_income"]) + tax_exempt_int = tax_unit_non_dep_add( + tax_unit, period, ["tax_exempt_interest_income"] + ) 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"]) + student_loan_int = tax_unit_non_dep_add( + tax_unit, period, ["student_loan_interest"] + ) mn_ald = max_(0, us_ald - student_loan_int) income = max_(0, sum_income - mn_ald) net_income = max_(0, income - income_amount) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_joint.py index 56cdef43d71..8e4356e2cb7 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_joint.py @@ -14,12 +14,13 @@ def formula(tax_unit, period, parameters): # Pool federal AGI at tax unit level agi = add(tax_unit, period, ["adjusted_gross_income_person"]) - # Pool Montana additions at tax unit level - additions = add(tax_unit, period, ["mt_additions"]) - - # Pool Montana subtractions at tax unit level - # This allows spouse's subtractions to offset head's income - subtractions = add(tax_unit, period, ["mt_subtractions"]) + # Pool the head's and spouse's Montana additions and subtractions. + # This allows spouse's subtractions to offset head's income. Federal + # AGI leaves out a tax unit dependent's income, which is on the + # dependent's own return, so the dependent's adjustments (such as + # their U.S. government interest) are left out too. + additions = tax_unit_non_dep_add(tax_unit, period, ["mt_additions"]) + subtractions = tax_unit_non_dep_add(tax_unit, period, ["mt_subtractions"]) # Apply pooled subtractions to pooled income reduced_agi = max_(agi + additions - subtractions, 0) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/base/mt_taxable_social_security_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/base/mt_taxable_social_security_joint.py index cebfa573b8f..f80126102eb 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/base/mt_taxable_social_security_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/base/mt_taxable_social_security_joint.py @@ -15,8 +15,11 @@ def formula(tax_unit, period, parameters): # Joint filers run the Taxable Social Security Benefits Schedule once # in a single column, combining both spouses' lines 1-9 and applying # the joint base amounts once (Form 2, page 6). + # The worksheet uses the head's and spouse's amounts from their federal + # return; a tax unit dependent's benefits, interest and adjustments are + # on the dependent's own return. # line 1 total net SS amount - social_security = add(tax_unit, period, ["social_security"]) + social_security = tax_unit_non_dep_add(tax_unit, period, ["social_security"]) # line 2 SS multiplied by the base rate social_security_benefits_fraction = social_security * p_irs.base.benefit_cap # line 3: irs_gross_income - taxable_social_security @@ -24,7 +27,7 @@ def formula(tax_unit, period, parameters): taxable_ss = add(tax_unit, period, ["taxable_social_security"]) reduced_gross_income = max_(gross_income - taxable_ss, 0) # line 5: tax exempt interest income - tax_exempt_interest_income = add( + tax_exempt_interest_income = tax_unit_non_dep_add( tax_unit, period, ["tax_exempt_interest_income"] ) # line 6: Sum of line 2, 3, 4, 5 @@ -34,11 +37,11 @@ def formula(tax_unit, period, parameters): + tax_exempt_interest_income ) # line 7: Remove the student loans from the above the line deductions - ald_less_student_loan = add( + ald_less_student_loan = tax_unit_non_dep_add( tax_unit, period, ["mt_applicable_ald_deductions"] - ) - add(tax_unit, period, ["student_loan_interest_ald"]) + ) - tax_unit_non_dep_add(tax_unit, period, ["student_loan_interest_ald"]) # line 8: Montana subtractions + ald - subtractions = add(tax_unit, period, ["mt_subtractions"]) + subtractions = tax_unit_non_dep_add(tax_unit, period, ["mt_subtractions"]) increased_subtractions = subtractions + ald_less_student_loan # line 9: line 6 - line 8, if line 8 >= line 6, return 0 income_reduced_by_subtractions = max_( diff --git a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_joint.py index d25a9981bc5..0a147c1233f 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_joint.py @@ -12,8 +12,10 @@ class mt_capital_gains_tax_applicable_threshold_joint(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.mt.tax.income.main.capital_gains - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) - stcg = add(tax_unit, period, ["short_term_capital_gains"]) + # The head's and spouse's gains: a tax unit dependent's are on the + # dependent's own return, as federal taxable income leaves them out. + ltcg = tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) + stcg = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) capital_gains = max_(min_(ltcg, ltcg + stcg), 0) taxable_income = add(tax_unit, period, ["mt_taxable_income_joint"]) filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.py index ac9960a67f9..86c865d14c2 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.py @@ -19,8 +19,10 @@ def formula(tax_unit, period, parameters): # Line 1 โ€” aggregate across persons for joint filing taxable_income = add(tax_unit, period, ["mt_taxable_income_joint"]) # Line 2 โ€” lesser of net LTCG and total net capital gain - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) - stcg = add(tax_unit, period, ["short_term_capital_gains"]) + # The head's and spouse's gains: a tax unit dependent's are on the + # dependent's own return, as federal taxable income leaves them out. + ltcg = tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) + stcg = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) net_cg = ltcg + stcg capital_gains = max_(min_(ltcg, net_cg), 0) # Line 3 diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/capital_gain/mt_capital_gain_credit.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/capital_gain/mt_capital_gain_credit.py index 8226eaf09cb..ffa4892ef25 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/capital_gain/mt_capital_gain_credit.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/capital_gain/mt_capital_gain_credit.py @@ -14,5 +14,8 @@ def formula(person, period, parameters): p = parameters(period).gov.states.mt.tax.income.credits.capital_gain net_capital_gain = person("capital_gains", period) + # A tax unit dependent's gains are on the dependent's own return, so + # the dependent has no credit on this one. + head_or_spouse = ~person("is_tax_unit_dependent", period) # The net capital gain variable is capped at 0 - return p.percentage * net_capital_gain + return p.percentage * net_capital_gain * head_or_spouse diff --git a/policyengine_us/variables/gov/states/mt/tax/income/exemptions/interest/mt_interest_exemption_person.py b/policyengine_us/variables/gov/states/mt/tax/income/exemptions/interest/mt_interest_exemption_person.py index 3bce8d6a88a..2c3d63c1bd7 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/exemptions/interest/mt_interest_exemption_person.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/exemptions/interest/mt_interest_exemption_person.py @@ -12,7 +12,11 @@ class mt_interest_exemption_person(Variable): def formula(person, period, parameters): # Allocate the interest exemption to head/spouse based on share of interest income. - interest_income = person("taxable_interest_income", period) + # The tax unit exemption counts only the head's and spouse's interest + # (mt_interest_exemption), so the shares do too: a tax unit dependent's + # interest is on the dependent's own return. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + interest_income = person("taxable_interest_income", period) * head_or_spouse total_interest_income = person.tax_unit.sum(interest_income) total_deduction = person.tax_unit("mt_interest_exemption", period) deduction_rate = np.zeros_like(total_interest_income) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_joint.py index 5dfab7c3707..036077b8d55 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_joint.py @@ -16,8 +16,10 @@ def formula(tax_unit, period, parameters): status = filing_status.possible_values if p.capital_gains.in_effect: - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) - stcg = add(tax_unit, period, ["short_term_capital_gains"]) + # The head's and spouse's gains: a tax unit dependent's are on the + # dependent's own return, as federal taxable income leaves them out. + ltcg = tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) + stcg = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) net_cg = ltcg + stcg # Montana Form 2 line 2 uses the federal net long-term capital gain # amount, which is limited by any short-term capital losses. diff --git a/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_ltcg_subtraction.py b/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_ltcg_subtraction.py index cadce9d85f4..940cb4f0963 100644 --- a/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_ltcg_subtraction.py +++ b/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_ltcg_subtraction.py @@ -14,12 +14,28 @@ class nd_ltcg_subtraction(Variable): "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2021-iit/individual-income-tax-booklet-2021.pdf#page=14", "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2022-iit/form-nd-1-2022.pdf#page=1", # line 7 "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2022-iit/2022-individual-income-tax-booklet.pdf#page=14", + "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2025-iit/2025-individual-income-tax-booklet.pdf#page=14", # line 6 + "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2025-iit/2025-individual-income-tax-booklet.pdf#page=15", # line 6 worksheet + "https://ndlegis.gov/cencode/t57c38.pdf#nameddest=57-38-30p3", # 2(d)(1) ) defined_for = StateCode.ND def formula(tax_unit, period, parameters): - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) - stcg = add(tax_unit, period, ["short_term_capital_gains"]) - net_ltcg = min_(ltcg, ltcg + stcg) + # N.D.C.C. 57-38-30.3(2)(d)(1) reduces the individual's federal taxable + # income by 40% of "the taxpayer's net long-term capital gain"; the + # Form ND-1 line 6 worksheet takes it from the filer's Schedule D. A tax + # unit dependent's gains are on the dependent's own return. + # Worksheet line 1 is Schedule D line 15, the net long-term gain with + # capital gain distributions, which are long-term (26 U.S.C. + # 852(b)(3)(B)) and go on line 3 when there is no Schedule D. Line 2 is + # Schedule D line 16, which adds the net short-term gain or loss. If + # either is zero or less, no exclusion is allowed. + long_term = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) + tax_unit_non_dep_add(tax_unit, period, ["non_sch_d_capital_gains"]) + short_term = tax_unit_non_dep_add( + tax_unit, period, ["short_term_capital_gains"] + ) + net_ltcg = max_(0, min_(long_term, long_term + short_term)) p = parameters(period).gov.states.nd.tax.income return net_ltcg * p.taxable_income.subtractions.ltcg_fraction diff --git a/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_qdiv_subtraction.py b/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_qdiv_subtraction.py index e8c8364f978..0b8652be9dd 100644 --- a/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_qdiv_subtraction.py +++ b/policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_qdiv_subtraction.py @@ -12,10 +12,16 @@ class nd_qdiv_subtraction(Variable): "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2021-iit/individual-income-tax-booklet-2021.pdf#page=15", "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2022-iit/form-nd-1-2022.pdf#page=1", # line 13 "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2022-iit/2022-individual-income-tax-booklet.pdf#page=15", + "https://www.tax.nd.gov/sites/www/files/documents/forms/individual/2025-iit/2025-individual-income-tax-booklet.pdf#page=15", # line 13 + "https://ndlegis.gov/cencode/t57c38.pdf#nameddest=57-38-30p3", # 2(d)(2) ) defined_for = StateCode.ND def formula(tax_unit, period, parameters): - qdiv = add(tax_unit, period, ["qualified_dividend_income"]) + # N.D.C.C. 57-38-30.3(2)(d)(2) reduces the individual's federal taxable + # income by 40% of qualified dividends; Form ND-1 line 13 takes them + # from Form 1040 line 3a. A tax unit dependent's dividends are on the + # dependent's own return, so only the head's and spouse's count. + qdiv = tax_unit_non_dep_add(tax_unit, period, ["qualified_dividend_income"]) p = parameters(period).gov.states.nd.tax.income return qdiv * p.taxable_income.subtractions.qdiv_fraction diff --git a/policyengine_us/variables/gov/states/nh/tax/income/nh_taxable_income.py b/policyengine_us/variables/gov/states/nh/tax/income/nh_taxable_income.py index 7f1a9ef1c3d..255b15f477f 100644 --- a/policyengine_us/variables/gov/states/nh/tax/income/nh_taxable_income.py +++ b/policyengine_us/variables/gov/states/nh/tax/income/nh_taxable_income.py @@ -13,7 +13,12 @@ class nh_taxable_income(Variable): ) defined_for = StateCode.NH - # New Hampshire allows for negative taxable income. - # It limits tax to nonnegative values in the tax computation instead. - adds = ["dividend_income", "interest_income"] - subtracts = ["nh_total_exemptions"] + def formula(tax_unit, period, parameters): + # The head's and spouse's interest and dividends: a tax unit dependent + # is a separate individual who reports their own on their own return. + income = tax_unit_non_dep_add( + tax_unit, period, ["dividend_income", "interest_income"] + ) + # New Hampshire allows for negative taxable income. + # It limits tax to nonnegative values in the tax computation instead. + return income - tax_unit("nh_total_exemptions", period) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py index a1b7752db0e..72e43e18425 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py @@ -13,4 +13,17 @@ class oh_deductions(Variable): "https://cms7files1.revize.com/starkcountyoh/Document_center/Offices/Auditor/Services/Homestead%20Exemption/Ohio_Adj_Gross_Income.pdf", ) defined_for = StateCode.OH - adds = "gov.states.oh.tax.income.deductions.deductions" + + def formula(person, period, parameters): + p = parameters(period).gov.states.oh.tax.income.deductions + total_subtractions = add(person, period, p.deductions) + # A tax unit dependent's U.S. government interest is on the dependent's + # own return and never in the filer's federal AGI, so it is not + # subtracted here, where each person's amounts are summed into the + # filer's. + if "us_govt_interest_person" in p.deductions: + dependent = person("is_tax_unit_dependent", period) + total_subtractions = total_subtractions - dependent * person( + "us_govt_interest_person", period + ) + return total_subtractions diff --git a/policyengine_us/variables/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.py b/policyengine_us/variables/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.py index 5e84425cb75..5bf88b18083 100644 --- a/policyengine_us/variables/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.py +++ b/policyengine_us/variables/gov/states/pa/tax/income/forgiveness/pa_eligibility_income.py @@ -9,7 +9,16 @@ class pa_eligibility_income(Variable): label = "PA eligibility income" unit = USD definition_period = YEAR - reference = "https://www.pa.gov/content/dam/copapwp-pagov/en/revenue/documents/formsandpublications/formsforindividuals/pit/documents/2021/2021_pa-40sp.pdf" + reference = ( + "https://www.pa.gov/content/dam/copapwp-pagov/en/revenue/documents/formsandpublications/formsforindividuals/pit/documents/2021/2021_pa-40sp.pdf", + "https://www.pa.gov/en/agencies/revenue/forms-and-publications/pa-personal-income-tax-guide/tax-forgiveness.html", + ) defined_for = StateCode.PA - adds = "gov.states.pa.tax.income.forgiveness.eligibility_income_sources" + def formula(tax_unit, period, parameters): + # Eligibility income is the claimant's, and the spouse's for a married + # claimant; a dependent child claims forgiveness on their own return + # (PA Personal Income Tax Guide, Tax Forgiveness). So only the head's + # and spouse's amounts count. + p = parameters(period).gov.states.pa.tax.income.forgiveness + return tax_unit_non_dep_add(tax_unit, period, p.eligibility_income_sources) diff --git a/policyengine_us/variables/gov/states/sc/tax/income/deductions/net_capital_gain/sc_net_capital_gain_deduction.py b/policyengine_us/variables/gov/states/sc/tax/income/deductions/net_capital_gain/sc_net_capital_gain_deduction.py index 307963dcb82..b457da287a7 100644 --- a/policyengine_us/variables/gov/states/sc/tax/income/deductions/net_capital_gain/sc_net_capital_gain_deduction.py +++ b/policyengine_us/variables/gov/states/sc/tax/income/deductions/net_capital_gain/sc_net_capital_gain_deduction.py @@ -15,8 +15,12 @@ class sc_net_capital_gain_deduction(Variable): ) def formula(tax_unit, period, parameters): - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) - stcg = add(tax_unit, period, ["short_term_capital_gains"]) + # S.C. Code 12-6-1150(A) deducts 44% of the net capital gain included + # in the individual's federal taxable income. A tax unit dependent's + # gains are on the dependent's own return, so only the head's and + # spouse's gains count. + ltcg = tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) + stcg = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) capped_stcg = min_(0, stcg) base = max_(0, ltcg + capped_stcg) p = parameters(period).gov.states.sc.tax.income.deductions.net_capital_gain diff --git a/policyengine_us/variables/gov/states/ut/tax/income/credits/child_tax_credit/ut_ctc_potential.py b/policyengine_us/variables/gov/states/ut/tax/income/credits/child_tax_credit/ut_ctc_potential.py index dd0e11dbdbb..7880fc85c12 100644 --- a/policyengine_us/variables/gov/states/ut/tax/income/credits/child_tax_credit/ut_ctc_potential.py +++ b/policyengine_us/variables/gov/states/ut/tax/income/credits/child_tax_credit/ut_ctc_potential.py @@ -21,7 +21,9 @@ def formula(tax_unit, period, parameters): base_amount = p.amount * eligible_children # Utah reduces the CTC based on the state income in addition to # tax exempt interest income - relevant_income = add( + # The head's and spouse's interest, as Utah taxable income leaves out + # a tax unit dependent's income. + relevant_income = tax_unit_non_dep_add( tax_unit, period, ["tax_exempt_interest_income", "ut_taxable_income"], diff --git a/policyengine_us/variables/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.py b/policyengine_us/variables/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.py index c47a3a17b5c..6470cd0e46f 100644 --- a/policyengine_us/variables/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.py +++ b/policyengine_us/variables/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.py @@ -17,12 +17,20 @@ def formula(tax_unit, period: Period, parameters): """ p = parameters(period).gov.states.ut.tax.income p_credit = p.credits.retirement - age = tax_unit.members("age", period) + person = tax_unit.members + age = person("age", period) birth_year = -(age - period.start.year) - meets_age_conditions = birth_year <= p_credit.birth_year + # The credit is for the claimant and, on a joint return, the spouse; + # a tax unit dependent is not a claimant on this return. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + meets_age_conditions = (birth_year <= p_credit.birth_year) & head_or_spouse max_value = p_credit.max * tax_unit.sum(meets_age_conditions) total_income = tax_unit("ut_total_income", period) - tax_exempt_interest = add(tax_unit, period, ["tax_exempt_interest_income"]) + # The head's and spouse's municipal bond interest, as Utah total + # income leaves out a tax unit dependent's income. + tax_exempt_interest = tax_unit_non_dep_add( + tax_unit, period, ["tax_exempt_interest_income"] + ) modified_agi = total_income + tax_exempt_interest filing_status = tax_unit("filing_status", period) phase_out_income = max_( diff --git a/policyengine_us/variables/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.py b/policyengine_us/variables/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.py index 8cf7f52a458..fa8fcaa6b2a 100644 --- a/policyengine_us/variables/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.py +++ b/policyengine_us/variables/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.py @@ -22,7 +22,9 @@ def formula(tax_unit, period, parameters): # Skip line 3 (muncipal bond interest) # Skip line 4 (subtract line 3 from line 2) - tax_exempt_interest = add( + # The head's and spouse's municipal bond interest, as Utah total + # income leaves out a tax unit dependent's income. + tax_exempt_interest = tax_unit_non_dep_add( tax_unit, period, ["tax_exempt_interest_income"] ) # Line 5 modified_agi = total_income + tax_exempt_interest # Line 6 diff --git a/policyengine_us/variables/gov/states/va/tax/income/va_subtractions.py b/policyengine_us/variables/gov/states/va/tax/income/va_subtractions.py index 884c338e747..b959254d017 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/va_subtractions.py +++ b/policyengine_us/variables/gov/states/va/tax/income/va_subtractions.py @@ -11,6 +11,9 @@ class va_subtractions(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.va.tax.income.subtractions - total_subtractions = add(tax_unit, period, p.subtractions) + # Virginia subtracts these items from federal adjusted gross income, + # which leaves out a tax unit dependent's income (the dependent files + # their own return), so only the head's and spouse's amounts count. + total_subtractions = tax_unit_non_dep_add(tax_unit, period, p.subtractions) # Prevent negative subtractions from acting as additions return max_(0, total_subtractions) diff --git a/policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py b/policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py index da551291e2c..fd364a3f52c 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py +++ b/policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py @@ -18,5 +18,8 @@ def formula(person, period, parameters): # the per-person amounts sum to it exactly. p = parameters(period).gov.states.va.tax.income.subtractions total_subtractions = add(person, period, p.subtractions) + # A tax unit dependent's income is on the dependent's own return, so + # the dependent has no subtractions on this one, as in va_subtractions. + head_or_spouse = ~person("is_tax_unit_dependent", period) # Prevent negative subtractions from acting as additions. - return max_(0, total_subtractions) + return max_(0, total_subtractions) * head_or_spouse diff --git a/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_percentage_capital_gains_exclusion.py b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_percentage_capital_gains_exclusion.py index d3e8aef242a..c6f222df2ac 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_percentage_capital_gains_exclusion.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_percentage_capital_gains_exclusion.py @@ -19,7 +19,11 @@ def formula(tax_unit, period, parameters): # depreciable personal property (except farm property/timber), and real estate. # Since standard capital gains inputs map to financial instruments (ineligible), # we only use explicitly designated VT-eligible capital gains. - eligible_gains = add( + # 32 V.S.A. 5811(21)(B)(ii) excludes 40% of gains on "assets held by + # the taxpayer for more than three years", to the extent included in + # federal adjusted gross income. A tax unit dependent's gains are on + # the dependent's own return, so only the head's and spouse's count. + eligible_gains = tax_unit_non_dep_add( tax_unit, period, ["long_term_capital_gains_on_assets_eligible_for_vt_exclusion"], diff --git a/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/vt_additions.py b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/vt_additions.py index a434ad17bb6..a78a265429f 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/vt_additions.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/vt_additions.py @@ -14,4 +14,9 @@ class vt_additions(Variable): "https://legislature.vermont.gov/statutes/section/32/151/05811", ) - adds = ["tax_exempt_interest_income"] + def formula(tax_unit, period, parameters): + # 32 V.S.A. 5811(21)(A)(i) adds the taxpayer's interest income from + # non-Vermont state and local obligations excluded from federal + # adjusted gross income. A tax unit dependent's interest is on the + # dependent's own return, so only the head's and spouse's count. + return tax_unit_non_dep_add(tax_unit, period, ["tax_exempt_interest_income"]) diff --git a/policyengine_us/variables/gov/states/wa/tax/income/millionaires_tax/wa_millionaires_tax_base_income.py b/policyengine_us/variables/gov/states/wa/tax/income/millionaires_tax/wa_millionaires_tax_base_income.py index 4989765b17e..ea7dbc9a28a 100644 --- a/policyengine_us/variables/gov/states/wa/tax/income/millionaires_tax/wa_millionaires_tax_base_income.py +++ b/policyengine_us/variables/gov/states/wa/tax/income/millionaires_tax/wa_millionaires_tax_base_income.py @@ -15,8 +15,14 @@ class wa_millionaires_tax_base_income(Variable): def formula(tax_unit, period, parameters): agi = tax_unit("adjusted_gross_income", period) - long_term_capital_gains = add(tax_unit, period, ["long_term_capital_gains"]) - tax_exempt_interest = add(tax_unit, period, ["tax_exempt_interest_income"]) + # Federal adjusted gross income leaves out a tax unit dependent's + # income, so the gains and interest are the head's and spouse's too. + long_term_capital_gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) + tax_exempt_interest = tax_unit_non_dep_add( + tax_unit, period, ["tax_exempt_interest_income"] + ) p = parameters(period).gov.states.wa.tax.income.capital_gains charitable_contributions = add( diff --git a/policyengine_us/variables/gov/states/wa/tax/income/wa_capital_gains_tax.py b/policyengine_us/variables/gov/states/wa/tax/income/wa_capital_gains_tax.py index c12a32defdd..d21b2f54e20 100644 --- a/policyengine_us/variables/gov/states/wa/tax/income/wa_capital_gains_tax.py +++ b/policyengine_us/variables/gov/states/wa/tax/income/wa_capital_gains_tax.py @@ -13,7 +13,10 @@ def formula(tax_unit, period, parameters): in_effect = parameters(period).gov.states.wa.tax.income.in_effect if in_effect: p = parameters(period).gov.states.wa.tax.income.capital_gains - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) + # The tax is on each individual's (and spouses' combined) long-term + # capital gains from the federal return. A tax unit dependent's + # gains are on the dependent's own return. + ltcg = tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) # Deduct charitable contributions. charitable_contributions = add( tax_unit, diff --git a/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_additions.py b/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_additions.py index 4f18cea6869..2e03a046c20 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_additions.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_additions.py @@ -14,4 +14,11 @@ class wi_additions(Variable): "https://www.revenue.wi.gov/TaxForms2022/2022-ScheduleAD-Inst.pdf", ) defined_for = StateCode.WI - adds = "gov.states.wi.tax.income.additions.sources" + + def formula(tax_unit, period, parameters): + # Wisconsin adds these items to federal adjusted gross income, which + # leaves out a tax unit dependent's income (the dependent files their + # own return), so only the head's and spouse's amounts count, such as + # their interest on other states' and municipalities' obligations. + p = parameters(period).gov.states.wi.tax.income.additions + return tax_unit_non_dep_add(tax_unit, period, p.sources) diff --git a/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_capital_loss.py b/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_capital_loss.py index da97129c235..8108e51be37 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_capital_loss.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/additions/wi_capital_loss.py @@ -16,7 +16,9 @@ class wi_capital_loss(Variable): def formula(tax_unit, period, parameters): # calculate Schedule WD, Line 18 GAIN_SOURCES = ["short_term_capital_gains", "long_term_capital_gains"] - netcg = add(tax_unit, period, GAIN_SOURCES) + # Only the head's and spouse's gains and losses are on this return; a + # tax unit dependent reports theirs on their own return. + netcg = tax_unit_non_dep_add(tax_unit, period, GAIN_SOURCES) # return Schedule WD, Line 28, as a positive amount as on form p = parameters(period).gov.states.wi.tax.income.additions filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_capital_gain_loss_subtraction.py b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_capital_gain_loss_subtraction.py index 3c001340416..c525078362e 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_capital_gain_loss_subtraction.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_capital_gain_loss_subtraction.py @@ -14,10 +14,13 @@ class wi_capital_gain_loss_subtraction(Variable): defined_for = StateCode.WI def formula(tax_unit, period, parameters): + # Schedule WD starts from the filer's federal Schedule D. A tax unit + # dependent's gains are on the dependent's own return, so only the + # head's and spouse's gains count. # calculate Schedule WD, Line 8 - stcg_net = add(tax_unit, period, ["short_term_capital_gains"]) + stcg_net = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) # calculate Schedule WD, Line 17 - ltcg_net = add(tax_unit, period, ["long_term_capital_gains"]) + ltcg_net = tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) # calculate Schedule WD, Line 18 totcg = max_(0, stcg_net + ltcg_net) # calculate Schedule WD, Line 20, the capital gain reduction diff --git a/policyengine_us/variables/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.py b/policyengine_us/variables/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.py index 2ec93c1a18e..04fec2fe9c8 100644 --- a/policyengine_us/variables/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.py +++ b/policyengine_us/variables/gov/states/wv/tax/income/credits/liftc/wv_low_income_family_tax_credit_agi.py @@ -7,11 +7,18 @@ class wv_low_income_family_tax_credit_agi(Variable): label = "Adjusted gross income for the West Virginia low-income family tax credit" unit = USD definition_period = YEAR - reference = "https://code.wvlegislature.gov/11-21-12/" # ยง11-21-12 (b) + # W. Va. Code 11-21-22a(d): "the federal adjusted gross income plus any + # applicable increasing West Virginia modifications plus any tax exempt + # interest income reported on the federal tax return". + reference = "https://code.wvlegislature.gov/11-21-22a/" defined_for = "wv_low_income_family_tax_credit_eligible" - adds = [ - "adjusted_gross_income", - "tax_exempt_interest_income", - "wv_additions", - ] + def formula(tax_unit, period, parameters): + # The tax-exempt interest is the head's and spouse's, reported on + # their federal return; a tax unit dependent's is on the dependent's + # own return, as federal adjusted gross income leaves it out. + return tax_unit_non_dep_add( + tax_unit, + period, + ["adjusted_gross_income", "tax_exempt_interest_income", "wv_additions"], + ) diff --git a/policyengine_us/variables/household/income/tax_unit/us_govt_interest.py b/policyengine_us/variables/household/income/tax_unit/us_govt_interest.py index 7604476910d..aa9e504e3b7 100644 --- a/policyengine_us/variables/household/income/tax_unit/us_govt_interest.py +++ b/policyengine_us/variables/household/income/tax_unit/us_govt_interest.py @@ -7,6 +7,19 @@ class us_govt_interest(Variable): label = "Interest on U.S. government obligations" unit = USD definition_period = YEAR - documentation = "Interest on U.S. government obligations such as U.S. savings bonds, U.S. Treasury bills, and U.S. government certificates." + documentation = "Interest on U.S. government obligations such as U.S. savings bonds, U.S. Treasury bills, and U.S. government certificates, received by the head and spouse." + reference = ( + # 31 U.S.C. 3124(a) exempts the interest from state income tax; states + # subtract it from federal adjusted gross income, which includes only + # the filer's own interest. + "https://www.law.cornell.edu/uscode/text/31/3124", + "https://legislature.vermont.gov/statutes/section/32/151/05811", # (21)(B)(i) + "https://ndlegis.gov/cencode/t57c38.pdf#nameddest=57-38-30p3", # 2(a) + ) - adds = ["us_govt_interest_person"] + def formula(tax_unit, period, parameters): + # States subtract this interest from federal adjusted gross income or + # taxable income. A tax unit dependent's interest is on the dependent's + # own return and never in the filer's federal AGI, so only the head's + # and spouse's interest counts. + return tax_unit_non_dep_add(tax_unit, period, ["us_govt_interest_person"]) From 435de86ab1e0426d6c5c9b44fa976096e8972b95 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 10:05:10 -0400 Subject: [PATCH 2/4] Use exact test amounts and leave the dependent's interest out of two UT cases The Montana and Utah Social Security credit cases now carry the exact half-cent amounts (1,765.725, 1,847.725 and 259.475) instead of values rounded past the 0.001 margin. In two existing Utah retirement credit cases person2 (age 64) is a tax unit dependent, so their tax-exempt interest is on their own return and no longer enters the modified AGI: 750 instead of 737.5, and 675 instead of 662.5. Co-Authored-By: Claude Opus 5.5 --- .../income/capital_gains/mt_capital_gains_tax_joint.yaml | 8 ++++---- .../retirement_credit/ut_retirement_credit_max.yaml | 9 +++++++-- .../ss_benefits_credit/ut_ss_benefits_credit_max.yaml | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml index b18389de370..b6c22336318 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_joint.yaml @@ -140,9 +140,9 @@ state_code: MT output: mt_capital_gains_tax_joint: 82 - mt_regular_income_tax_joint: 1_765.72 + mt_regular_income_tax_joint: 1_765.725 mt_capital_gains_tax_applicable_threshold_joint: 0 - mt_income_tax: 1_847.72 + mt_income_tax: 1_847.725 - name: Same household without the dependent's gains period: 2025 @@ -159,6 +159,6 @@ state_code: MT output: mt_capital_gains_tax_joint: 82 - mt_regular_income_tax_joint: 1_765.72 + mt_regular_income_tax_joint: 1_765.725 mt_capital_gains_tax_applicable_threshold_joint: 0 - mt_income_tax: 1_847.72 + mt_income_tax: 1_847.725 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml index 0812b78f10f..02b73857bd3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/retirement_credit/ut_retirement_credit_max.yaml @@ -65,7 +65,10 @@ members: [person1, person2] state_code: UT output: - ut_retirement_credit_max: 737.5 # phase-out reduction partially eliminates credit: 900-(30000+3*500-25000)/0.025 = 737.5 + # person1 is the head, person3 the spouse and person2 a dependent, whose + # tax-exempt interest is on their own return. Phase-out reduction: + # 900 - (30_000 + 2 * 500 - 25_000) * 0.025 = 750. + ut_retirement_credit_max: 750 - name: UT retirement credit max unit test 4 period: 2022 @@ -115,7 +118,9 @@ members: [person1, person2] state_code: UT output: - ut_retirement_credit_max: 662.5 + # person2 is a dependent, so only the head's and spouse's tax-exempt + # interest counts: 900 - (40_000 + 2 * 500 - 32_000) * 0.025 = 675. + ut_retirement_credit_max: 675 - name: Edge case test, birth year period: 2022 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml index 875e5d85c1c..9f521aaeaf0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ss_benefits_credit/ut_ss_benefits_credit_max.yaml @@ -68,7 +68,7 @@ household: state_code: UT output: - ut_ss_benefits_credit_max: 259.48 + ut_ss_benefits_credit_max: 259.475 - name: Same household without the dependent's interest period: 2022 @@ -84,4 +84,4 @@ household: state_code: UT output: - ut_ss_benefits_credit_max: 259.48 + ut_ss_benefits_credit_max: 259.475 From 6ae711ae21e8bee44311a0543dd34f2ec42eda87 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 14:17:20 -0400 Subject: [PATCH 3/4] Leave dependents' own items out of DC, DE and OH subtractions DC, Delaware and Ohio sum per-person subtractions into the filers' state AGI. A tax unit dependent's own items, which irs_gross_income leaves off the filers' federal return, reduced the parents' state tax. Mask every item for dependents except the Ohio 529 and medical deductions the filer pays for them (new parameter oh...filer_amounts_recorded_on_dependents), through a new person_non_dep_add helper. This subsumes #9894's special case for U.S. government interest. Also count two per-return amounts once: - dc_disability_exclusion returned the return's D-2440 exclusion to every member; it now shares it between the filers by capped payments and leaves out dependents' payments. - oh_unreimbursed_medical_care_expense_deduction_person added the tax-unit insured expense deduction, which `adds` projects onto every member; it now adds each person's prorated share. Co-Authored-By: Claude Opus 5.5 --- ...ndent-state-subtractions-dc-de-oh.fixed.md | 1 + .../filer_amounts_recorded_on_dependents.yaml | 21 + ...dependent_state_subtractions_invariants.py | 370 ++++++++++++++++++ .../tax/income/dc_disability_exclusion.yaml | 65 ++- .../dc/tax/income/dc_income_subtractions.yaml | 79 ++++ .../income/subtractions/de_subtractions.yaml | 76 ++++ .../tax/income/deductions/oh_deductions.yaml | 77 ++++ policyengine_us/tools/general.py | 23 ++ .../subtractions/dc_disability_exclusion.py | 24 +- .../subtractions/dc_income_subtractions.py | 18 +- .../income/subtractions/de_subtractions.py | 17 +- ...d_medical_care_expense_deduction_person.py | 4 +- .../oh/tax/income/deductions/oh_deductions.py | 21 +- 13 files changed, 755 insertions(+), 41 deletions(-) create mode 100644 changelog.d/dependent-state-subtractions-dc-de-oh.fixed.md create mode 100644 policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml create mode 100644 policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py diff --git a/changelog.d/dependent-state-subtractions-dc-de-oh.fixed.md b/changelog.d/dependent-state-subtractions-dc-de-oh.fixed.md new file mode 100644 index 00000000000..9ae748688f9 --- /dev/null +++ b/changelog.d/dependent-state-subtractions-dc-de-oh.fixed.md @@ -0,0 +1 @@ +Leave a tax unit dependent's own items (unemployment compensation, state tax refunds, disability exclusions, Pell grants, disability benefits, Ohio section 179, uniformed services retirement and educator deductions, and Delaware 529 contributions) out of the DC, Delaware and Ohio subtractions pooled into the filers' state AGI. Count the DC disability income exclusion and the Ohio insured medical expense deduction once per return instead of once per member. diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml new file mode 100644 index 00000000000..42c403d4781 --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml @@ -0,0 +1,21 @@ +description: Ohio deducts these per-person amounts from the filer's federal adjusted gross income even when they are recorded on a tax unit dependent, as they are the filer's amounts. Other deductions on a dependent are the dependent's own and belong on their own return. +values: + 2021-01-01: + # The filer's 529 contributions. oh_529_plan_deduction caps the tax unit's + # contributions at $4,000 per beneficiary, and each person's share + # follows the contributions recorded on them, as in the other states' + # 529 deductions. + - oh_529_plan_deduction_person + # Premiums and medical care the taxpayer paid "for the taxpayer, the + # taxpayer's spouse, and dependents" (R.C. 5747.01(A)(10)), recorded on + # the person they covered. + - oh_unreimbursed_medical_care_expense_deduction_person +metadata: + unit: list + period: year + label: Ohio deductions summed over dependents + reference: + - title: Ohio Revised Code 5747.01(A)(9) and (A)(10) + href: https://codes.ohio.gov/ohio-revised-code/section-5747.01 + - title: Ohio 2025 IT 1040 Instructions, Schedule of Adjustments lines 37 and 44 + href: https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=24 diff --git a/policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py b/policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py new file mode 100644 index 00000000000..a6fe6518c12 --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py @@ -0,0 +1,370 @@ +"""Property tests: a dependent's own items stay off the filers' DC, Delaware +and Ohio returns, and per-return amounts are counted once. + +DC, Delaware and Ohio subtract per-person items from each person's federal +adjusted gross income and pool every member into the filer's state AGI +(dc_taxable_income_joint, de_agi_joint, oh_agi). Federal adjusted gross income +leaves out a tax unit dependent's income (irs_gross_income); the dependent +reports it, and takes these subtractions, on their own return. Each subtracted +item is the taxpayer's own: + +- DC subtracts income "included in federal gross income" or entered from the + filer's federal return (D.C. Code 47-1803.02(a)(2)(A), (E), (L), (LL); D-40 + lines 9, 10 and 13), the D-2440 disability income exclusion ((M)), one per + return with a column for you and one for your spouse, and up to $10,000 of + a disabled person's own income ((V)). +- Delaware subtracts income "to the extent included in federal adjusted gross + income" (30 Del. C. 1106(b)(4), (9), (10)) and 529 contributions made by + the individual, or by the spouses on a joint return ((b)(11)). +- Ohio deducts the taxpayer's own income and expenses (R.C. 5747.01(A)(4), + (11), (18), (23), (27), (31)), and the medical and 529 amounts the taxpayer + paid, including for dependents ((A)(9), (A)(10)). + +So for every tax unit in these states, and any income items its dependents +have (unemployment compensation, state tax refunds, U.S. government interest, +D-2440 disability payments, SSI, Pell grants, disability benefits, Ohio +section 179 add-backs, uniformed services retirement and educator expenses): + +1. DC, Delaware and Ohio AGI, taxable income and tax before credits are the + same as when the dependents have none of these items. +2. A dependent's dc_income_subtractions and de_subtractions are zero, and + their oh_deductions are their shares of the filer's 529 and medical + deductions. +3. Per-return amounts are counted once over the tax unit: + - the Ohio unreimbursed medical deduction is the members' medical expenses + over 7.5% of federal AGI; + - the DC disability exclusion is the filers' capped payments less federal + AGI over taxable Social Security plus $15,000. + +Dependents are 1 to 17, so they are qualifying children with no income test. +Amounts are whole dollars of at most $200,000. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2025 +TOLERANCE = 0.01 + +# A dependent's own items, zeroed in the twin. +OWN_ITEMS = [ + "unemployment_compensation", + "salt_refund_income", + "taxable_interest_income", + "us_govt_interest_person", + "total_disability_payments", + "ssi", + "pell_grant", + "disability_benefits", + "oh_section_179_expense_add_back", + "oh_uniformed_services_retirement_income_deduction", + "oh_educator_expense_deduction_person", +] +# The filer's amounts, recorded on whoever they were for. +FILER_ITEMS = [ + "other_medical_expenses", + "investment_in_529_plan_indv", + "count_529_contribution_beneficiaries", +] +SAME_AS_WITHOUT_DEPENDENT_ITEMS = { + "DC": [ + "dc_taxable_income_joint", + "dc_income_tax_before_credits", + ], + "DE": [ + "de_income_tax_before_non_refundable_credits_unit", + ], + "OH": [ + "oh_agi", + "oh_taxable_income", + "oh_income_tax_before_non_refundable_credits", + ], +} +UNIT_OUTPUTS = sorted( + {v for variables in SAME_AS_WITHOUT_DEPENDENT_ITEMS.values() for v in variables} + | { + "adjusted_gross_income", + "tax_unit_taxable_social_security", + "oh_unreimbursed_medical_care_expense_deduction", + } +) +PERSON_OUTPUTS = [ + "dc_income_subtractions", + "dc_disability_exclusion", + "de_subtractions", + "de_agi_joint", + "oh_deductions", + "oh_529_plan_deduction_person", + "oh_unreimbursed_medical_care_expense_deduction_person", + "is_tax_unit_dependent", +] + + +def amount(low, high): + return st.integers(low, high).map(float) + + +def maybe(draw, high): + """Zero half the time, so items also appear alone.""" + return draw(st.one_of(st.just(0.0), amount(0, high))) + + +@st.composite +def items(draw): + interest = maybe(draw, 20_000) + return { + "unemployment_compensation": maybe(draw, 20_000), + "salt_refund_income": maybe(draw, 5_000), + "taxable_interest_income": interest, + "us_govt_interest_person": draw(amount(0, int(interest))), + "total_disability_payments": maybe(draw, 8_000), + "ssi": maybe(draw, 10_000), + "pell_grant": maybe(draw, 7_000), + "disability_benefits": maybe(draw, 20_000), + "oh_section_179_expense_add_back": maybe(draw, 5_000), + "oh_uniformed_services_retirement_income_deduction": maybe(draw, 30_000), + "oh_educator_expense_deduction_person": maybe(draw, 300), + "other_medical_expenses": maybe(draw, 20_000), + "investment_in_529_plan_indv": maybe(draw, 5_000), + "count_529_contribution_beneficiaries": draw(st.integers(0, 2)), + } + + +@st.composite +def tax_units(draw): + state = draw(st.sampled_from(["DC", "DE", "OH"])) + n_filers = draw(st.integers(1, 2)) + filers = [ + { + "age": draw(st.integers(25, 64)), + # Low wages too, where the D-2440 exclusion applies. + "employment_income": draw(amount(0, 200_000)), + "is_permanently_and_totally_disabled": draw(st.booleans()), + **draw(items()), + } + for _ in range(n_filers) + ] + dependents = [ + { + "age": draw(st.integers(1, 17)), + "is_permanently_and_totally_disabled": draw(st.booleans()), + **draw(items()), + } + for _ in range(draw(st.integers(1, 3))) + ] + return {"state": state, "filers": filers, "dependents": dependents} + + +def without_dependent_items(unit): + zero = {name: 0.0 for name in OWN_ITEMS} + return {**unit, "dependents": [{**d, **zero} for d in unit["dependents"]]} + + +def build_situation(units): + """One tax unit per household: the head, an optional spouse and the + dependents, each dependent in a marital unit of their own.""" + people, tax_units_out, marital_units, households = {}, {}, {}, {} + for i, unit in enumerate(units): + members = [] + for j, person in enumerate(unit["filers"] + unit["dependents"]): + name = f"person_{i}_{j}" + filer = j < len(unit["filers"]) + people[name] = { + "is_tax_unit_head": {YEAR: j == 0}, + "is_tax_unit_spouse": {YEAR: j == 1 and filer}, + "is_tax_unit_dependent": {YEAR: not filer}, + **{key: {YEAR: value} for key, value in person.items()}, + } + members.append(name) + if not filer: + marital_units[f"dependent_{i}_{j}"] = {"members": [name]} + tax_units_out[f"tax_unit_{i}"] = {"members": members} + marital_units[f"filers_{i}"] = {"members": members[: len(unit["filers"])]} + households[f"household_{i}"] = { + "members": members, + "state_code": {YEAR: unit["state"]}, + } + return { + "people": people, + "tax_units": tax_units_out, + "marital_units": marital_units, + "households": households, + } + + +def calculate(units): + simulation = Simulation(situation=build_situation(units)) + unit_results = { + variable: np.asarray(simulation.calculate(variable, YEAR)) + for variable in UNIT_OUTPUTS + } + person_results = { + variable: np.asarray(simulation.calculate(variable, YEAR)) + for variable in PERSON_OUTPUTS + } + p = simulation.tax_benefit_system.parameters(YEAR).gov + return unit_results, person_results, p + + +def unit_index(units): + """The tax unit of each person, in the order build_situation adds them.""" + return np.concatenate( + [ + np.full(len(unit["filers"]) + len(unit["dependents"]), i) + for i, unit in enumerate(units) + ] + ) + + +def unit_sum(values, index, n): + return np.bincount(index, weights=values, minlength=n) + + +def person_inputs(units, name): + return np.array( + [ + person[name] + for unit in units + for person in unit["filers"] + unit["dependents"] + ] + ) + + +def check(units): + twins = [without_dependent_items(unit) for unit in units] + unit_results, person_results, p = calculate(units + twins) + n = len(units) + states = np.array([unit["state"] for unit in units]) + index = unit_index(units) + n_people = len(index) + people = {name: values[:n_people] for name, values in person_results.items()} + person_state = states[index] + dependent = people["is_tax_unit_dependent"].astype(bool) + + # 1. The same state AGI, taxable income and tax as without the + # dependents' own items. + for state, variables in SAME_AS_WITHOUT_DEPENDENT_ITEMS.items(): + in_state = states == state + for variable in variables: + np.testing.assert_allclose( + unit_results[variable][:n][in_state], + unit_results[variable][n:][in_state], + atol=TOLERANCE, + err_msg=variable, + ) + in_de = person_state == "DE" + np.testing.assert_allclose( + unit_sum(people["de_agi_joint"], index, n)[states == "DE"], + unit_sum(person_results["de_agi_joint"][n_people:], index, n)[states == "DE"], + atol=TOLERANCE, + err_msg="de_agi_joint", + ) + + # 2. Dependents' rows. + in_dc = person_state == "DC" + in_oh = person_state == "OH" + assert np.all(people["dc_income_subtractions"][dependent & in_dc] == 0) + assert np.all(people["de_subtractions"][dependent & in_de] == 0) + np.testing.assert_allclose( + people["oh_deductions"][dependent & in_oh], + ( + people["oh_529_plan_deduction_person"] + + people["oh_unreimbursed_medical_care_expense_deduction_person"] + )[dependent & in_oh], + atol=TOLERANCE, + ) + + # 3. Per-return amounts counted once. + agi = unit_results["adjusted_gross_income"][:n] + is_oh = states == "OH" + medical = unit_sum(person_inputs(units, "other_medical_expenses"), index, n) + oh_rate = p.states.oh.tax.income.deductions.unreimbursed_medical_care_expenses.rate + expected_medical = np.maximum(0, medical - oh_rate * agi) + np.testing.assert_allclose( + unit_sum( + people["oh_unreimbursed_medical_care_expense_deduction_person"], index, n + )[is_oh], + expected_medical[is_oh], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + unit_results["oh_unreimbursed_medical_care_expense_deduction"][:n][is_oh], + expected_medical[is_oh], + atol=TOLERANCE, + ) + + is_dc = states == "DC" + d2440 = p.irs.income.disability_income_exclusion + filer = ~dependent + capped = filer * np.minimum( + person_inputs(units, "total_disability_payments"), d2440.cap + ) + reduced = np.maximum( + 0, + agi - unit_results["tax_unit_taxable_social_security"][:n] - d2440.amount, + ) + expected_d2440 = np.maximum(0, unit_sum(capped, index, n) - reduced) + np.testing.assert_allclose( + unit_sum(people["dc_disability_exclusion"], index, n)[is_dc], + expected_d2440[is_dc], + atol=TOLERANCE, + ) + assert np.all(people["dc_disability_exclusion"][dependent & in_dc] == 0) + + +# A simulation's cost is mostly fixed, so each example is a batch of tax +# units and there are few examples. +@settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(tax_units(), min_size=10, max_size=30)) +def test_dependent_items_stay_off_dc_de_and_oh_returns(units): + check(units) + + +def test_reported_examples(): + """A head with $60,000 of wages and a dependent child with their own + unemployment compensation, state tax refund, disability exclusion and + Ohio items, in each state; and an Ohio household whose medical deduction + used to be given to every member.""" + zero = {name: 0.0 for name in OWN_ITEMS + FILER_ITEMS} + filer = { + "age": 40, + "employment_income": 60_000.0, + "is_permanently_and_totally_disabled": False, + **zero, + } + child = { + "age": 15, + "is_permanently_and_totally_disabled": True, + **zero, + "unemployment_compensation": 10_000.0, + "salt_refund_income": 5_000.0, + "ssi": 9_000.0, + "pell_grant": 3_000.0, + "disability_benefits": 3_000.0, + "oh_section_179_expense_add_back": 3_000.0, + } + units = [ + {"state": state, "filers": [filer], "dependents": [child]} + for state in ["DC", "DE", "OH"] + ] + units.append( + { + "state": "OH", + "filers": [{**filer, "other_medical_expenses": 6_000.0}], + "dependents": [{**zero, "age": 10, "other_medical_expenses": 3_000.0}], + } + ) + check(units) + unit_results, person_results, _ = calculate(units) + # DC: $60,000 less the $22,500 head of household standard deduction. + assert unit_results["dc_taxable_income_joint"][0] == 37_500 + # Ohio: 9,000 of medical expenses over 7.5% of 60,000, once. + assert unit_results["oh_unreimbursed_medical_care_expense_deduction"][3] == 4_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml index 911618083d2..ffd3c81ee4b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml @@ -1,5 +1,10 @@ +# Form D-2440 is one per return: line 4 adds the capped payments of you and +# your spouse, and line 10 subtracts federal AGI less taxable Social Security +# less $15,000. Each filer gets a share of the line 10 exclusion in proportion +# to their capped payments, so the return's exclusion is counted once. - name: disability payments are over the cap,and the reduced income is positive period: 2021 + absolute_error_margin: 0.01 input: people: person1: @@ -19,7 +24,9 @@ members: [person1, person2] state_code: DC output: - dc_disability_exclusion: 200 + # Line 4: 5,200 + 4,000 = 9,200. Line 9: 30,000 - 6,000 - 15,000 = 9,000. + # Line 10: 200, split 5,200:4,000. + dc_disability_exclusion: [113.04, 86.96] - name: disability payments are over the cap, and the reduced income is negative period: 2021 @@ -42,7 +49,8 @@ members: [person1, person2] state_code: DC output: - dc_disability_exclusion: 10_400 + # Line 10: 5,200 + 5,200 = 10,400, as 20,000 - 6,000 - 15,000 < 0. + dc_disability_exclusion: [5_200, 5_200] - name: disability payments are under the cap, and the reduced income is negative period: 2021 @@ -65,4 +73,55 @@ members: [person1, person2] state_code: DC output: - dc_disability_exclusion: 7_000 + dc_disability_exclusion: [4_000, 3_000] + +- name: The exclusion is counted once in DC AGI + # Line 9: 18,000 - 15,000 = 3,000, so line 10 is 5,000 - 3,000 = 2,000. + # It used to go to every member, so a household of three subtracted 6,000. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 50 + employment_income: 18_000 + total_disability_payments: 5_000 + spouse: + is_tax_unit_spouse: true + age: 50 + child: + age: 10 + tax_units: + tax_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: DC + output: + dc_disability_exclusion: [2_000, 0, 0] + dc_income_subtractions: [2_000, 0, 0] + dc_agi: [16_000, 0, 0] + +- name: A dependent's disability payments are on their own return + period: 2021 + input: + people: + head: + is_tax_unit_head: true + total_disability_payments: 4_000 + child: + is_tax_unit_dependent: true + total_disability_payments: 5_000 + tax_units: + tax_unit: + members: [head, child] + adjusted_gross_income: 20_000 + tax_unit_taxable_social_security: 0 + households: + household: + members: [head, child] + state_code: DC + output: + # Line 4 is the head's 4,000. Line 9: 20,000 - 15,000 = 5,000. Line 10: 0. + dc_disability_exclusion: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml index 2da5757b53a..71d1fdbeeb0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_income_subtractions.yaml @@ -16,3 +16,82 @@ state_code: DC output: dc_income_subtractions: [200, 0] + +- name: A dependent's unemployment compensation and state tax refund are not subtracted + # D-40 line 13 subtracts unemployment benefits "included on Line 4" (federal + # AGI) and line 9 takes refunds from the filer's own federal return. The + # child's are on the child's own return. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + unemployment_compensation: 1_000 + salt_refund_income: 300 + child: + age: 16 + unemployment_compensation: 10_000 + salt_refund_income: 5_000 + household: + state_code: DC + output: + dc_income_subtractions: [1_300, 0] + +- name: A dependent's persons-with-disabilities exclusion is not subtracted + # D.C. Code 47-1803.02(a)(2)(V) excludes the disabled person's own income. + # The disabled head gets the $10,000 exclusion, plus $6,200 of taxable + # Social Security Disability (D-40 line 10). The disabled child's exclusion + # would be on the child's own return. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 30_000 + is_permanently_and_totally_disabled: true + social_security_disability: 12_000 + child: + is_tax_unit_dependent: true + age: 10 + is_permanently_and_totally_disabled: true + ssi: 9_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: DC + output: + dc_disabled_exclusion_subtraction: [10_000, 10_000] + dc_income_subtractions: [16_200, 0] + +- name: Without the head's disability, a disabled child gives the filer no exclusion + # Taxable income is $30,000 of wages less the $22,500 head of household + # standard deduction. Counting the child's exclusion took it to -$2,500. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 30_000 + child: + is_tax_unit_dependent: true + age: 10 + is_permanently_and_totally_disabled: true + ssi: 9_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: DC + output: + dc_disabled_exclusion_subtraction: [0, 10_000] + dc_income_subtractions: [0, 0] + dc_taxable_income_joint: 7_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml index 4ec19202c05..5639c450173 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/de_subtractions.yaml @@ -16,3 +16,79 @@ state_code: DE output: de_subtractions: [200, 0] + +- name: A dependent's state tax refund is not subtracted + # 30 Del. C. 1106(b)(9) subtracts a Delaware refund "to the extent included + # in federal adjusted gross income". The child's is on the child's return. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + salt_refund_income: 300 + child: + age: 16 + salt_refund_income: 5_000 + household: + state_code: DE + output: + de_subtractions: [300, 0] + +- name: A dependent's 2021 unemployment compensation is not subtracted + # 30 Del. C. 1106(b)(10): 2020 and 2021 unemployment benefits, "to the + # extent included in federal adjusted gross income". + period: 2021 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + unemployment_compensation: 1_000 + child: + age: 16 + unemployment_compensation: 10_000 + household: + state_code: DE + output: + de_subtractions: [1_000, 0] + +- name: A dependent's 529 contribution is not subtracted + # 30 Del. C. 1106(b)(11): amounts "contributed by an individual", or by the + # spouses on a joint return. A contributor's subtraction is on their own + # return. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + investment_in_529_plan_indv: 300 + child: + age: 16 + investment_in_529_plan_indv: 800 + household: + state_code: DE + output: + de_subtractions: [300, 0] + +- name: A dependent's own items leave Delaware taxable income unchanged + # Taxable income is the head's $60,000 less the $3,250 standard deduction, + # as without the child's refund. Counting the refund made it $51,750. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + child: + age: 16 + salt_refund_income: 5_000 + household: + state_code: DE + output: + de_taxable_income_joint: [56_750, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml index d0ea8b2f9c3..e9ad4fffadc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml @@ -16,3 +16,80 @@ state_code: OH output: oh_deductions: [200, 0] + +- name: A dependent's own income and expenses are not deducted + # Each is the dependent's own and on the dependent's own return: section 179 + # add-backs (R.C. 5747.01(A)(18)), uniformed services retired pay "received + # by the taxpayer" ((A)(23)), Pell grant amounts "included in your federal + # adjusted gross income" (2025 IT 1040 instructions, line 38), the educator + # expenses of "a taxpayer who is a qualifying Ohio educator" ((A)(31)), + # disability benefits "to the extent included in federal adjusted gross + # income" ((A)(4)) and refunds of the taxpayer's itemized deductions + # ((A)(11)(a)). + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + salt_refund_income: 300 + child: + is_tax_unit_dependent: true + is_tax_unit_spouse: false + age: 19 + is_full_time_college_student: true + oh_section_179_expense_add_back: 1_000 + oh_uniformed_services_retirement_income_deduction: 2_000 + pell_grant: 3_000 + oh_educator_expense_deduction_person: 300 + disability_benefits: 4_000 + salt_refund_income: 500 + household: + state_code: OH + output: + oh_deductions: [300, 0] + +- name: Medical expenses the filer paid for a dependent are deducted once + # R.C. 5747.01(A)(10)(b): medical care "of the taxpayer, the taxpayer's + # spouse, and dependents" over 7.5% of federal AGI: 9,000 - 4,500 = 4,500. + # The tax unit's deduction used to be given to every member. + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + other_medical_expenses: 6_000 + child: + age: 10 + other_medical_expenses: 3_000 + household: + state_code: OH + output: + oh_unreimbursed_medical_care_expense_deduction_person: [3_000, 1_500] + oh_unreimbursed_medical_care_expense_deduction: 4_500 + oh_deductions: [3_000, 1_500] + oh_agi: 55_500 + +- name: A 529 contribution recorded on a dependent is deducted + # The tax unit's $1,000 contribution for one beneficiary is under the + # $4,000 limit (R.C. 5747.01(A)(9), 5747.70). + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 60_000 + child: + age: 10 + investment_in_529_plan_indv: 1_000 + count_529_contribution_beneficiaries: 1 + household: + state_code: OH + output: + oh_529_plan_deduction_person: [0, 1_000] + oh_deductions: [0, 1_000] + oh_agi: 59_000 diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index b730a0a30ca..c9d0a5331ad 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -79,6 +79,29 @@ def person_share_of_tax_unit_amount(person, period, tax_unit_variable, person_va return where(filer, filer_share, own) +def person_non_dep_add(person, period, variables, include_dependents=()): + """ + Add person-level variables for each person, leaving out dependents' own. + + Like `add` over people, for per-person amounts that are later pooled into + the filer's return. A tax unit dependent's own items belong on their own + return, as irs_gross_income leaves their income off this one, so a + dependent gets zero for every variable except those named in + `include_dependents`: the filer's amounts even when recorded on a + dependent, such as medical expenses the filer paid for them. Variables + must be person-level; `add` rejects others. + """ + filer = ~person("is_tax_unit_dependent", period) + total = 0 + for variable in variables: + amount = add(person, period, [variable]) + if variable in include_dependents: + total = total + amount + else: + total = total + filer * amount + return total + + 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/states/dc/tax/income/subtractions/dc_disability_exclusion.py b/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_disability_exclusion.py index 355a56af7c1..2dd1c0bbf7f 100644 --- a/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_disability_exclusion.py +++ b/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_disability_exclusion.py @@ -9,20 +9,32 @@ class dc_disability_exclusion(Variable): definition_period = YEAR defined_for = StateCode.DC reference = ( - "https://code.dccouncil.gov/us/dc/council/code/sections/47-1803.02#(a)(2)(M)" + "https://code.dccouncil.gov/us/dc/council/code/sections/47-1803.02#(a)(2)(M)", + "https://otr.cfo.dc.gov/sites/default/files/dc/sites/otr/publication/attachments/2022_D-2440.pdf#page=1", ) def formula(person, period, parameters): p = parameters(period).gov.irs.income.disability_income_exclusion tax_unit = person.tax_unit - + # Form D-2440 is one per return, with a column for you and one for + # your spouse. A dependent's disability payments are on their own + # return. + filer = ~person("is_tax_unit_dependent", period) disability_payments = person("total_disability_payments", period) - capped_disability_payments = min_(disability_payments, p.cap) + # Up to $5,200 per disabled person. + capped_disability_payments = filer * min_(disability_payments, p.cap) + # Line 4. total_capped_payments = tax_unit.sum(capped_disability_payments) - + # Lines 5 to 9. federal_agi = tax_unit("adjusted_gross_income", period) social_security_income = tax_unit("tax_unit_taxable_social_security", period) reduced_income = federal_agi - social_security_income - p.amount capped_reduced_income = max_(reduced_income, 0) - - return max_(total_capped_payments - capped_reduced_income, 0) + # Line 10. + exclusion = max_(total_capped_payments - capped_reduced_income, 0) + # dc_taxable_income_joint sums every person, so each filer gets a + # share of the return's exclusion in proportion to their payments. + share = np.zeros_like(total_capped_payments) + mask = total_capped_payments > 0 + share[mask] = capped_disability_payments[mask] / total_capped_payments[mask] + return exclusion * share diff --git a/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py b/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py index cc41f847bf5..5568d9b8b88 100644 --- a/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py +++ b/policyengine_us/variables/gov/states/dc/tax/income/subtractions/dc_income_subtractions.py @@ -15,15 +15,13 @@ class dc_income_subtractions(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.dc.tax.income.subtractions - total_subtractions = add(person, period, p.sources) - # A tax unit dependent's U.S. government interest is on the dependent's - # own return and never in the filer's federal AGI, so it is not - # subtracted here, where each person's amounts are summed into the - # filer's. - if "us_govt_interest_person" in p.sources: - dependent = person("is_tax_unit_dependent", period) - total_subtractions = total_subtractions - dependent * person( - "us_govt_interest_person", period - ) + # Each person's DC AGI is summed into the filer's + # (dc_taxable_income_joint), so a tax unit dependent gets none of + # these. Each item is the dependent's own and belongs on their own + # return: income "included in federal gross income" or entered from + # the filer's federal return (D.C. Code 47-1803.02(a)(2)(A), (E), (L) + # and (LL); D-40 lines 9, 10 and 13), and exclusions of a disabled + # person's own income ((M), Form D-2440, and (V)). + total_subtractions = person_non_dep_add(person, period, p.sources) # Prevent negative subtractions from acting as additions return max_(0, total_subtractions) diff --git a/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py b/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py index a4a8b2cf107..0816e6c268c 100644 --- a/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py +++ b/policyengine_us/variables/gov/states/de/tax/income/subtractions/de_subtractions.py @@ -11,15 +11,12 @@ class de_subtractions(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.de.tax.income.subtractions - total_subtractions = add(person, period, p.subtractions) - # A tax unit dependent's U.S. government interest is on the dependent's - # own return and never in the filer's federal AGI, so it is not - # subtracted here, where each person's amounts are summed into the - # filer's. - if "us_govt_interest_person" in p.subtractions: - dependent = person("is_tax_unit_dependent", period) - total_subtractions = total_subtractions - dependent * person( - "us_govt_interest_person", period - ) + # Each person's net income is summed into the filer's (de_agi_joint), + # so a tax unit dependent gets none of these. The income items are + # subtracted "to the extent included in federal adjusted gross income" + # (30 Del. C. 1106(b)(4), (9), (10)), which leaves a dependent's income + # out, and the 529 subtraction is for amounts "contributed by an + # individual" or, on a joint return, "by the spouses" ((b)(11)). + total_subtractions = person_non_dep_add(person, period, p.subtractions) # Prevent negative subtractions from acting as additions return max_(0, total_subtractions) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py index d20a345b203..69621671bc2 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py @@ -14,8 +14,10 @@ class oh_unreimbursed_medical_care_expense_deduction_person(Variable): ) defined_for = StateCode.OH + # Each person's share of the tax unit's insured expense deduction, so the + # one deduction per return is not repeated for every member. adds = [ - "oh_insured_unreimbursed_medical_care_expenses", + "oh_insured_unreimbursed_medical_care_expenses_person", "oh_uninsured_unreimbursed_medical_care_expenses", "long_term_health_insurance_premiums", ] diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py index 72e43e18425..d6a1d98ebf0 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/oh_deductions.py @@ -16,14 +16,13 @@ class oh_deductions(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.oh.tax.income.deductions - total_subtractions = add(person, period, p.deductions) - # A tax unit dependent's U.S. government interest is on the dependent's - # own return and never in the filer's federal AGI, so it is not - # subtracted here, where each person's amounts are summed into the - # filer's. - if "us_govt_interest_person" in p.deductions: - dependent = person("is_tax_unit_dependent", period) - total_subtractions = total_subtractions - dependent * person( - "us_govt_interest_person", period - ) - return total_subtractions + # Each person's Ohio AGI is summed into the filer's (oh_agi), so a tax + # unit dependent gets only the filer's amounts recorded on them, such + # as medical expenses the filer paid for them. The dependent's own + # income and expenses belong on their own return. + return person_non_dep_add( + person, + period, + p.deductions, + include_dependents=p.filer_amounts_recorded_on_dependents, + ) From 5386c7ccd9b82c0dd6272e33b9d1b3d9d9e32b57 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 18:41:07 -0400 Subject: [PATCH 4/4] Address review: DC household test, D-2440 case, 529 wording - Property test: leave DC units with a disabled filer out of the twin invariant, since D.C. Code 47-1803.02(a)(2)(V) tests household AGI, which counts a dependent's income; add a fixed DC couple with a positive D-2440 exclusion and a dependent with payments. - Describe Ohio's 529 dependent share as a modelling convention that matches the pooled tax-unit deduction. - person_non_dep_add starts from a zero array; fix a test name typo. Co-Authored-By: Claude Opus 5.5 --- .../filer_amounts_recorded_on_dependents.yaml | 13 +++-- ...dependent_state_subtractions_invariants.py | 48 +++++++++++++++++-- .../tax/income/dc_disability_exclusion.yaml | 2 +- .../tax/income/deductions/oh_deductions.yaml | 7 +-- policyengine_us/tools/general.py | 2 +- 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml index 42c403d4781..49a28e030a8 100644 --- a/policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml +++ b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/filer_amounts_recorded_on_dependents.yaml @@ -1,10 +1,13 @@ -description: Ohio deducts these per-person amounts from the filer's federal adjusted gross income even when they are recorded on a tax unit dependent, as they are the filer's amounts. Other deductions on a dependent are the dependent's own and belong on their own return. +description: Ohio deducts these per-person amounts from the filer's federal adjusted gross income even when they are recorded on a tax unit dependent. Other deductions on a dependent are the dependent's own and belong on their own return. values: 2021-01-01: - # The filer's 529 contributions. oh_529_plan_deduction caps the tax unit's - # contributions at $4,000 per beneficiary, and each person's share - # follows the contributions recorded on them, as in the other states' - # 529 deductions. + # A modelling convention: oh_529_plan_deduction caps the contributions + # recorded on every tax unit member at $4,000 per beneficiary, as the + # other states' 529 deductions pool them, and each member's share of + # that tax-unit deduction follows their contributions. Masking a + # dependent's share would leave the shares short of the tax-unit amount. + # investment_in_529_plan_indv is documented per contributor, so what an + # amount on a dependent means is an open question across states. - oh_529_plan_deduction_person # Premiums and medical care the taxpayer paid "for the taxpayer, the # taxpayer's spouse, and dependents" (R.C. 5747.01(A)(10)), recorded on diff --git a/policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py b/policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py index a6fe6518c12..11c9b718aef 100644 --- a/policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py +++ b/policyengine_us/tests/core/test_dependent_state_subtractions_invariants.py @@ -26,7 +26,11 @@ section 179 add-backs, uniformed services retirement and educator expenses): 1. DC, Delaware and Ohio AGI, taxable income and tax before credits are the - same as when the dependents have none of these items. + same as when the dependents have none of these items. DC units with a + disabled filer are left out: the (V) exclusion's limit on household + adjusted gross income counts every household member's income, so a + dependent's income can rightly end a filer's exclusion (the model tests + tax-unit AGI for now). 2. A dependent's dc_income_subtractions and de_subtractions are zero, and their oh_deductions are their shares of the filer's 529 and medical deductions. @@ -245,9 +249,18 @@ def check(units): dependent = people["is_tax_unit_dependent"].astype(bool) # 1. The same state AGI, taxable income and tax as without the - # dependents' own items. + # dependents' own items, except DC units with a disabled filer, whose + # household income test counts the dependents' income. + disabled_filer = np.array( + [ + any( + filer["is_permanently_and_totally_disabled"] for filer in unit["filers"] + ) + for unit in units + ] + ) for state, variables in SAME_AS_WITHOUT_DEPENDENT_ITEMS.items(): - in_state = states == state + in_state = (states == state) & ~((states == "DC") & disabled_filer) for variable in variables: np.testing.assert_allclose( unit_results[variable][:n][in_state], @@ -331,8 +344,9 @@ def test_dependent_items_stay_off_dc_de_and_oh_returns(units): def test_reported_examples(): """A head with $60,000 of wages and a dependent child with their own unemployment compensation, state tax refund, disability exclusion and - Ohio items, in each state; and an Ohio household whose medical deduction - used to be given to every member.""" + Ohio items, in each state; an Ohio household whose medical deduction used + to be given to every member; and a DC couple with a positive D-2440 + exclusion, which used to be given to every member too.""" zero = {name: 0.0 for name in OWN_ITEMS + FILER_ITEMS} filer = { "age": 40, @@ -362,9 +376,33 @@ def test_reported_examples(): "dependents": [{**zero, "age": 10, "other_medical_expenses": 3_000.0}], } ) + units.append( + { + "state": "DC", + "filers": [ + { + **filer, + "employment_income": 8_000.0, + "total_disability_payments": 6_000.0, + }, + { + **filer, + "employment_income": 0.0, + "total_disability_payments": 3_000.0, + }, + ], + "dependents": [{**zero, "age": 10, "total_disability_payments": 5_000.0}], + } + ) check(units) unit_results, person_results, _ = calculate(units) # DC: $60,000 less the $22,500 head of household standard deduction. assert unit_results["dc_taxable_income_joint"][0] == 37_500 # Ohio: 9,000 of medical expenses over 7.5% of 60,000, once. assert unit_results["oh_unreimbursed_medical_care_expense_deduction"][3] == 4_500 + # D-2440: line 4 is 5,200 + 3,000 (the child's payments are on the + # child's return), and line 9 is zero, as federal AGI of 8,000 is under + # $15,000. Line 10 is 8,200 for the return, shared 5,200 and 3,000. + np.testing.assert_allclose( + person_results["dc_disability_exclusion"][-3:], [5_200, 3_000, 0] + ) diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml index ffd3c81ee4b..f8d08552dc4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_disability_exclusion.yaml @@ -2,7 +2,7 @@ # your spouse, and line 10 subtracts federal AGI less taxable Social Security # less $15,000. Each filer gets a share of the line 10 exclusion in proportion # to their capped payments, so the return's exclusion is counted once. -- name: disability payments are over the cap,and the reduced income is positive +- name: disability payments are over the cap, and the reduced income is positive period: 2021 absolute_error_margin: 0.01 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml index e9ad4fffadc..3072723afcf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/oh_deductions.yaml @@ -73,9 +73,10 @@ oh_deductions: [3_000, 1_500] oh_agi: 55_500 -- name: A 529 contribution recorded on a dependent is deducted - # The tax unit's $1,000 contribution for one beneficiary is under the - # $4,000 limit (R.C. 5747.01(A)(9), 5747.70). +- name: A 529 contribution recorded on a dependent stays in the tax unit's deduction + # oh_529_plan_deduction pools the contributions recorded on every member, + # here $1,000 for one beneficiary, under the $4,000 limit (R.C. + # 5747.01(A)(9), 5747.70). The child keeps their share of it. period: 2025 input: people: diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index c9d0a5331ad..978524a6a1b 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -92,7 +92,7 @@ def person_non_dep_add(person, period, variables, include_dependents=()): must be person-level; `add` rejects others. """ filer = ~person("is_tax_unit_dependent", period) - total = 0 + total = np.zeros(person.count) for variable in variables: amount = add(person, period, [variable]) if variable in include_dependents: