diff --git a/changelog.d/fix-state-head-spouse-swap.fixed.md b/changelog.d/fix-state-head-spouse-swap.fixed.md new file mode 100644 index 00000000000..114f6f0a6da --- /dev/null +++ b/changelog.d/fix-state-head-spouse-swap.fixed.md @@ -0,0 +1 @@ +Make state income taxes independent of which spouse is labelled head: dependents' income on Arkansas, Delaware, Iowa, Mississippi and Montana separate-column returns goes to the spouse with the greater income (an exact tie splits it); Mississippi splits a tax-unit health savings account or self-employed deduction with no per-person amounts equally between the spouses; Delaware chooses the dependent credit split and dependent care credit column together with EITC routing by greater remaining tax under 30 Del. C. 1117(b) to minimise tax; Montana's joint itemized deductions count both spouses' mortgage and investment interest; and an incapacitated or disabled head now qualifies as the other spouse's qualifying individual for Minnesota's dependent care credit, Montana's dependent care deduction and Oregon's working family household and dependent care credit, a surviving-spouse claimant for Missouri's property tax credit, and a totally disabled spouse for Oklahoma's property tax credit. Delaware credit-choice scoring uses the stored calculation precision to preserve this invariance at high incomes. Delaware separate-column credit calculations use pre-ordering CDCC and EITC amounts, with limits applied to the separate columns rather than the joint-return tax. diff --git a/policyengine_us/model_api.py b/policyengine_us/model_api.py index 56abf55953c..5bcf5df7f0c 100644 --- a/policyengine_us/model_api.py +++ b/policyengine_us/model_api.py @@ -55,6 +55,43 @@ def allocate_joint_amount_to_minimize_combined_tax( return best_head_allocation +def move_dependent_amounts_to_filer(person, period, amount): + """Puts the tax unit dependents' `amount` on a filer's own column. + + Some states' formulas count dependents' income on their filers' return, + although a dependent mostly reports it on their own return, which the + model does not compute. Where spouses file separately on one return, this + is a modelling convention for which column gets those amounts, not a state + rule: the spouse whose own `amount` is greater takes the dependents' + total, and an exact tie splits it equally, so the result does not depend + on which spouse is labelled head. It borrows the federal rule that a + child's income reported by parents who file separately goes on the return + of the parent with the greater taxable income (26 U.S.C. 1(g)(5)(B); IRS + Form 8814 instructions). Without a spouse the head takes it. Dependents + keep nothing of their own. + + Assumes each tax unit has one head and at most one spouse, as the role + variables assign; with two spouses the total would not be conserved. + """ + tax_unit = person.tax_unit + is_dependent = person("is_tax_unit_dependent", period) + is_head = person("is_tax_unit_head", period) + is_spouse = person("is_tax_unit_spouse", period) + dependents_amount = tax_unit.sum(is_dependent * amount) + head_amount = tax_unit.sum(is_head * amount) + spouse_amount = tax_unit.sum(is_spouse * amount) + own = where(is_head, head_amount, spouse_amount) + other = where(is_head, spouse_amount, head_amount) + has_spouse = tax_unit.any(is_spouse) + share = where( + has_spouse, + where(own > other, 1, where(own == other, 0.5, 0)), + 1, + ) + filer_share = (is_head | is_spouse) * share + return where(is_dependent, 0, amount) + filer_share * dependents_amount + + STATES = [ "AL", "AK", diff --git a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py index d4f8dce8cd5..7f05a4bf64d 100644 --- a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py +++ b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py @@ -8,18 +8,9 @@ REVIEWED_APPLIED_CREDIT_EXTERNAL_REFERENCES = { - "de_cdcc": { - # Delaware combined separate (FS4) routes the applied CDCC to the - # lower-income spouse's column (PIT-RES Line 31); it is re-capped at - # that column's tax in de_income_tax_after_non_refundable_credits_indv. - "variables/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.py", - }, "de_non_refundable_eitc": { "variables/gov/states/de/tax/income/credits/eitc/de_eitc.py", "variables/gov/states/de/tax/income/credits/eitc/refundability_calculation/de_income_tax_if_claiming_non_refundable_eitc.py", - # Combined separate (FS4) path applies the EITC to the higher-income - # spouse's column (PIT-RES Line 34), capped at that column's tax. - "variables/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.py", }, "az_dependent_tax_credit": { # Arizona refundable dependent credit contrib reform references diff --git a/policyengine_us/tests/core/test_ms_health_savings_account_adjustment_invariants.py b/policyengine_us/tests/core/test_ms_health_savings_account_adjustment_invariants.py index 8c34b72a919..109a9967ecc 100644 --- a/policyengine_us/tests/core/test_ms_health_savings_account_adjustment_invariants.py +++ b/policyengine_us/tests/core/test_ms_health_savings_account_adjustment_invariants.py @@ -24,8 +24,8 @@ 2. Bounds: no share is negative or above the tax unit's deduction, and a dependent's is zero whatever their own amount. 3. Attribution: when the head's and spouse's own amounts sum to the - deduction, each gets their own amount; when they have none, the head gets - all of it. + deduction, each gets their own amount; when they have none, the head and + spouse split it equally, whichever is labelled head. 4. The deduction lowers the members' total Mississippi AGI by at least zero and at most the deduction itself. (On main it could lower it by more.) """ @@ -215,6 +215,9 @@ def _run(units, year, *, zero_deduction): np.asarray(sim.calculate(name, year), dtype=float) for name in others ) person["is_head"] = np.asarray(sim.calculate("is_tax_unit_head", year), dtype=bool) + person["is_spouse"] = np.asarray( + sim.calculate("is_tax_unit_spouse", year), dtype=bool + ) person["is_dependent"] = np.asarray( sim.calculate("is_tax_unit_dependent", year), dtype=bool ) @@ -252,7 +255,7 @@ def _check(units, year): assert (shares[person["is_dependent"]] == 0).all() # 3. Attribution: own amounts when they sum to the deduction; otherwise, - # without any, the head's. + # without any, an equal share for the head and the spouse. filer = ~person["is_dependent"] filers_own = _unit_sum( person, person["health_savings_account_ald_person"] * filer, n @@ -263,9 +266,13 @@ def _check(units, year): person["health_savings_account_ald_person"][matches], atol=TOLERANCE, ) - no_own_head = (filers_own == 0)[unit] & person["is_head"] + head_or_spouse = person["is_head"] | person["is_spouse"] + n_spouses = _unit_sum(person, head_or_spouse.astype(float), n) + no_own = (filers_own == 0)[unit] & head_or_spouse np.testing.assert_allclose( - shares[no_own_head], deduction[unit][no_own_head], atol=TOLERANCE + shares[no_own], + (deduction / np.maximum(n_spouses, 1))[unit][no_own], + atol=TOLERANCE, ) # 4. The deduction lowers total Mississippi AGI by no more than itself. diff --git a/policyengine_us/tests/core/test_state_tax_head_spouse_swap_invariance.py b/policyengine_us/tests/core/test_state_tax_head_spouse_swap_invariance.py new file mode 100644 index 00000000000..6a2c81e71e9 --- /dev/null +++ b/policyengine_us/tests/core/test_state_tax_head_spouse_swap_invariance.py @@ -0,0 +1,584 @@ +"""State income taxes do not depend on which spouse is labelled head. + +Which spouse a married couple lists first on a return is a convention of the +form. States tax "the taxpayer and spouse" alike, and spouses who file +separately on one return each report their own income in their own column +(Arkansas Filing Status 4, Delaware Filing Status 4, Iowa status 3, +Mississippi's combined return, Montana Filing Status 2a). So exchanging the +two adults' `is_tax_unit_head` and `is_tax_unit_spouse` labels, with every +other input kept, must leave each state's income tax unchanged. + +On main these depended on the label: +- Arkansas, Delaware, Iowa, Mississippi and Montana put the dependents' income + the model counts on the head's column before taxing each column separately. + It now goes on the column of the spouse with the greater income of their + own, and an exact tie splits it (move_dependent_amounts_to_filer). +- Delaware's combined separate return routed the EITC and the dependent care + credit by label on equal incomes and split the dependent personal credits + with a heuristic. They are now chosen together to minimise tax. +- Montana's joint itemized deductions counted only the head's mortgage and + investment interest. +- Minnesota's child and dependent care credit and Montana's dependent care + deduction accepted an incapacitated spouse but not an incapacitated head. +- Missouri's property tax credit tested the survivor pathway, and Oklahoma's + the disability pathway, on the head only. +- Oregon's working family household and dependent care credit excluded a + disabled head as a qualifying spouse and counted only the head's earnings. +- Mississippi's health savings account and self-employed adjustments put a + tax-unit deduction with no per-person split on the head's column. + +Couples where a spouse is claimed as a dependent on another return are out +of scope here: the federal standard deduction and several state formulas +read only `head_is_dependent_elsewhere`, so this test leaves that input at +its default. + +Hypothesis draws batches of married couples, with and without dependents +(who may have income), in the nine states above for 2021-2026. A seeded +population of those states, with crafted edge cases (offsetting incomes, +spouses with equal income, one disabled, incapacitated, elderly or surviving +spouse, interest paid by one spouse), adds breadth in every year, and three +seeded couples in every state guard current law (2026) everywhere else. Each +batch runs as one vectorized simulation that holds every couple twice, as +drawn and with the head and spouse labels exchanged. For each couple: + +1. Swap invariance: state income tax, before and after refundable credits, + is finite and the same to the cent. +2. The per-person columns (`ar_agi_indiv`, `de_agi_indiv`, `ia_net_income`, + `ms_agi`, `mt_agi_indiv`) are the same for each person, to the cent. +3. Differential: in Arkansas, Delaware, Iowa and Mississippi those columns + equal an independent numpy allocation of each person's own amount, which + conserves the tax unit's total. Montana's own amounts are not + reconstructed, so it is checked by 1 and 2 only. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.model_api import STATES + +TOLERANCE = 0.01 # dollars: the same to the cent +# The differential compares float32 model values with float64 numpy sums of +# them, so it also allows float32 rounding. +FLOAT32_RTOL = 1e-6 +YEARS = [2021, 2022, 2023, 2024, 2025, 2026] +AFFECTED = ["AR", "DE", "IA", "MN", "MO", "MS", "MT", "OK", "OR"] +ALL_STATES = [s for s in STATES if s not in ("PR", "VI")] +TAX_UNIT_OUTPUTS = [ + "state_income_tax", + "state_income_tax_before_refundable_credits", +] +COLUMNS = ["ar_agi_indiv", "de_agi_indiv", "ia_net_income", "ms_agi", "mt_agi_indiv"] + + +def money(high): + return st.one_of(st.just(0.0), st.integers(1, high).map(float)) + + +def gain_or_loss(low, high): + return st.one_of(st.just(0.0), st.integers(low, high).map(float)) + + +@st.composite +def adults(draw): + return { + "age": draw(st.integers(18, 90)), + "is_blind": draw(st.booleans()), + "is_disabled": draw(st.booleans()), + "employment_income": draw(money(150_000)), + "self_employment_income": draw(gain_or_loss(-20_000, 80_000)), + "taxable_interest_income": draw(money(10_000)), + "qualified_dividend_income": draw(money(10_000)), + "long_term_capital_gains": draw(gain_or_loss(-5_000, 50_000)), + "rental_income": draw(gain_or_loss(-10_000, 30_000)), + "social_security_retirement": draw(money(40_000)), + "taxable_private_pension_income": draw(money(50_000)), + "taxable_ira_distributions": draw(money(30_000)), + "military_retirement_pay": draw(money(30_000)), + "unemployment_compensation": draw(money(12_000)), + "traditional_ira_contributions": draw(money(7_000)), + "student_loan_interest": draw(money(2_500)), + "real_estate_taxes": draw(money(12_000)), + "deductible_mortgage_interest": draw(money(20_000)), + "investment_interest_expense": draw(money(5_000)), + "charitable_cash_donations": draw(money(8_000)), + "other_medical_expenses": draw(money(15_000)), + "is_incapable_of_self_care": draw(st.booleans()), + "pre_subsidy_care_expenses": draw(money(6_000)), + "social_security_survivors": draw(money(20_000)), + "is_fully_disabled_service_connected_veteran": draw(st.booleans()), + } + + +@st.composite +def dependents(draw): + age = draw(st.integers(0, 23)) + return { + "age": age, + "is_full_time_student": age >= 18, + "employment_income": draw(money(25_000)), + "self_employment_income": draw(gain_or_loss(-3_000, 8_000)), + "taxable_interest_income": draw(money(3_000)), + "qualified_dividend_income": draw(money(3_000)), + "long_term_capital_gains": draw(gain_or_loss(-2_000, 5_000)), + "pre_subsidy_childcare_expenses": draw(money(10_000)), + # The model counts these on the filers' return in Delaware and + # Montana; no formula computes them. + "de_additions": draw(money(3_000)), + "mt_additions": draw(money(3_000)), + } + + +@st.composite +def couples(draw): + return { + "state": draw(st.sampled_from(AFFECTED)), + "adults": [draw(adults()), draw(adults())], + "dependents": draw(st.lists(dependents(), max_size=3)), + # Tax-unit deductions with no per-person split, as microdata supply + # the health savings account deduction. + "tax_unit": { + "health_savings_account_ald": draw(money(8_550)), + "self_employed_health_insurance_ald": draw(money(6_000)), + }, + } + + +SEED = 20261007 + + +def _seeded_adult(rng): + def some(high, p): + return float(round(rng.uniform(1, high))) if rng.random() < p else 0.0 + + def signed(low, high, p): + return float(round(rng.uniform(low, high))) if rng.random() < p else 0.0 + + age = int(rng.integers(18, 91)) + return { + "age": age, + "is_blind": bool(rng.random() < 0.1), + "is_disabled": bool(rng.random() < 0.1), + "employment_income": some(150_000, 0.7), + "self_employment_income": signed(-20_000, 80_000, 0.25), + "taxable_interest_income": some(10_000, 0.4), + "qualified_dividend_income": some(10_000, 0.25), + "long_term_capital_gains": signed(-5_000, 50_000, 0.2), + "rental_income": signed(-10_000, 30_000, 0.12), + "social_security_retirement": some(40_000, 0.5 if age >= 62 else 0.03), + "taxable_private_pension_income": some(50_000, 0.45 if age >= 55 else 0.05), + "taxable_ira_distributions": some(30_000, 0.2 if age >= 59 else 0.03), + "military_retirement_pay": some(30_000, 0.04), + "unemployment_compensation": some(12_000, 0.06), + "traditional_ira_contributions": some(7_000, 0.15), + "student_loan_interest": some(2_500, 0.1), + "real_estate_taxes": some(12_000, 0.35), + "deductible_mortgage_interest": some(20_000, 0.25), + "investment_interest_expense": some(5_000, 0.08), + "charitable_cash_donations": some(8_000, 0.3), + "other_medical_expenses": some(15_000, 0.2), + "is_incapable_of_self_care": bool(rng.random() < 0.05), + "pre_subsidy_care_expenses": some(6_000, 0.05), + "social_security_survivors": some(20_000, 0.05 if age >= 55 else 0.0), + "is_fully_disabled_service_connected_veteran": bool(rng.random() < 0.03), + } + + +def _seeded_dependent(rng): + def some(high, p): + return float(round(rng.uniform(1, high))) if rng.random() < p else 0.0 + + age = int(rng.integers(0, 24)) + return { + "age": age, + "is_full_time_student": age >= 18, + "employment_income": some(25_000, 0.5 if age >= 14 else 0.0), + "self_employment_income": some(8_000, 0.1 if age >= 14 else 0.0), + "taxable_interest_income": some(3_000, 0.3), + "qualified_dividend_income": some(3_000, 0.15), + "long_term_capital_gains": some(5_000, 0.08), + "pre_subsidy_childcare_expenses": some(10_000, 0.5 if age < 13 else 0.0), + "de_additions": some(3_000, 0.1), + "mt_additions": some(3_000, 0.1), + } + + +def _review_cases(): + # The cases in the review of PolicyEngine/policyengine-us#9617: adults + # with $60,000 and $10,000 in Arkansas and $100,000 and $30,000 in + # Delaware, each with a dependent earning $20,000. + def adult(wages): + return {"age": 45, "employment_income": wages} + + dependent = {"age": 17, "employment_income": 20_000.0} + return [ + { + "state": "AR", + "adults": [adult(60_000.0), adult(10_000.0)], + "dependents": [dependent], + }, + { + "state": "DE", + "adults": [adult(100_000.0), adult(30_000.0)], + "dependents": [dependent], + }, + # Review r1 of #9981: float64 credit-choice scores hid a float32 + # difference in the downstream columns at high incomes. + { + "state": "DE", + "adults": [ + {"age": 60, "employment_income": 1_015_250.0}, + {"age": 45, "employment_income": 1_015_250.0}, + ], + "dependents": [{"age": 10, "pre_subsidy_childcare_expenses": 190.20}], + }, + ] + + +def _edge_cases(states): + # Couples that random draws rarely produce, in each of the states. + def adult(age=45, **amounts): + return {"age": age, **amounts} + + teen = {"age": 17, "employment_income": 9_000.0, "taxable_interest_income": 900.0} + itemized = { + "real_estate_taxes": 9_000.0, + "deductible_mortgage_interest": 30_000.0, + "charitable_cash_donations": 4_000.0, + } + couples = [ + # Offsetting self-employment income, so the couple's total is zero. + ( + [ + adult(self_employment_income=30_000.0, **itemized), + adult(self_employment_income=-30_000.0), + ], + [teen], + ), + # Spouses with equal income and a dependent with income. + ( + [adult(employment_income=45_000.0), adult(employment_income=45_000.0)], + [teen], + ), + ([adult(), adult()], [teen]), + # One spouse has all the income; the dependent too. + ( + [adult(employment_income=90_000.0), adult()], + [teen, {"age": 15, "taxable_interest_income": 2_000.0}], + ), + # Interest paid by one spouse, enough to itemize. + ( + [ + adult(employment_income=120_000.0), + adult( + employment_income=30_000.0, + **itemized, + investment_interest_expense=6_000.0, + taxable_interest_income=9_000.0, + ), + ], + [], + ), + # Low income, property tax, one totally disabled spouse under 65. + ( + [ + adult(40, employment_income=6_000.0, real_estate_taxes=900.0), + adult(40, is_disabled=True), + ], + [], + ), + # One spouse 65 or older, the other young, low income. + ( + [ + adult( + 70, social_security_retirement=14_000.0, real_estate_taxes=2_500.0 + ), + adult(50, employment_income=8_000.0), + ], + [], + ), + # One blind spouse. + ( + [ + adult(80, social_security_retirement=20_000.0, is_blind=True), + adult(30, employment_income=25_000.0), + ], + [], + ), + # One incapacitated spouse with care expenses; the other earns and + # pays property tax (review of #9981). + ( + [ + adult(employment_income=20_000.0, real_estate_taxes=12_000.0), + adult( + is_incapable_of_self_care=True, pre_subsidy_care_expenses=4_000.0 + ), + ], + [], + ), + # Equal incomes, different ages and a child without income: credit + # routing on ties (review of #9981). + ( + [ + adult(60, employment_income=20_000.0), + adult(45, employment_income=20_000.0), + ], + [{"age": 10}], + ), + # A surviving spouse aged 60 with a low-income spouse who pays + # property tax (review of #9981). + ( + [ + adult(60, employment_income=6_000.0, real_estate_taxes=900.0), + adult(60, social_security_survivors=6_000.0), + ], + [], + ), + # One disabled spouse who cannot care for themselves, with care + # expenses; the other earns (review round 2 of #9981). + ( + [ + adult(employment_income=20_000.0), + adult( + is_disabled=True, + is_incapable_of_self_care=True, + pre_subsidy_care_expenses=4_000.0, + ), + ], + [], + ), + # One spouse a 100% disabled veteran, low income, rent. + ( + [ + adult(55, employment_income=8_000.0, rent=7_200.0), + adult(55, is_fully_disabled_service_connected_veteran=True), + ], + [], + ), + ] + edge = [ + {"state": state, "adults": adults, "dependents": deps} + for state in states + for adults, deps in couples + ] + # A tax-unit health savings account deduction with no per-person split + # and unequal incomes, as in the microdata (#9617 cross-check). + edge += [ + { + "state": state, + "adults": [ + adult(employment_income=300_000.0), + adult(employment_income=2_000.0), + ], + "dependents": [teen], + "tax_unit": {"health_savings_account_ald": 9_000.0}, + } + for state in states + ] + return edge + + +def _seeded_couples(states, per_state): + rng = np.random.default_rng(SEED) + return [ + { + "state": state, + "adults": [_seeded_adult(rng), _seeded_adult(rng)], + # Most couples have dependents. + "dependents": [ + _seeded_dependent(rng) for _ in range(int(rng.choice([0, 1, 1, 2, 3]))) + ], + "tax_unit": { + "health_savings_account_ald": ( + float(round(rng.uniform(1, 8_550))) if rng.random() < 0.3 else 0.0 + ), + }, + } + for state in states + for _ in range(per_state) + ] + + +def _situation(units, year): + people = {} + groups = { + "tax_units": {}, + "spm_units": {}, + "families": {}, + "marital_units": {}, + "households": {}, + } + for i, unit in enumerate(units): + # The second copy exchanges the head and spouse labels; people keep + # their order and every other input. + for copy in (0, 1): + a, b = f"a_{i}_{copy}", f"b_{i}_{copy}" + for name, amounts, is_head in ( + (a, unit["adults"][0], copy == 0), + (b, unit["adults"][1], copy == 1), + ): + people[name] = { + **amounts, + "is_tax_unit_head": is_head, + "is_tax_unit_spouse": not is_head, + "is_tax_unit_dependent": False, + } + members = [a, b] + groups["marital_units"][f"couple_{i}_{copy}"] = {"members": [a, b]} + for j, amounts in enumerate(unit["dependents"]): + child = f"dependent_{i}_{copy}_{j}" + people[child] = { + **amounts, + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + } + members.append(child) + groups["marital_units"][f"single_{i}_{copy}_{j}"] = {"members": [child]} + for group, prefix in ( + ("tax_units", "tax_unit"), + ("spm_units", "spm_unit"), + ("families", "family"), + ): + groups[group][f"{prefix}_{i}_{copy}"] = {"members": members} + groups["tax_units"][f"tax_unit_{i}_{copy}"].update( + {k: {year: v} for k, v in unit.get("tax_unit", {}).items()} + ) + groups["households"][f"household_{i}_{copy}"] = { + "members": members, + "state_code": {year: unit["state"]}, + } + people = { + name: {k: {year: v} for k, v in values.items()} + for name, values in people.items() + } + return {"people": people, **groups} + + +def _own_amounts(sim, year, column): + # Each person's own amount before dependents' amounts are moved, from the + # column's inputs. + def calc(name): + return np.asarray(sim.calculate(name, year), dtype=float) + + if column == "ar_agi_indiv": + return np.maximum(calc("ar_gross_income_indiv") - calc("ar_exemptions"), 0) + if column == "de_agi_indiv": + return np.maximum( + calc("de_pre_exclusions_agi") + - calc("de_elderly_or_disabled_income_exclusion_indiv"), + 0, + ) + if column == "ia_net_income": + return calc("ia_gross_income") - calc("ia_income_adjustments") + if column == "ms_agi": + p = sim.tax_benefit_system.parameters(year).gov.states.ms.tax.income + net = sum(calc(source) for source in p.income_sources) - calc( + "ms_agi_adjustments" + ) + dependent = calc("is_tax_unit_dependent").astype(bool) + return np.where(dependent, np.maximum(net, 0), net) + raise ValueError(column) + + +def _reference_allocation(own, unit, head, spouse, dependent): + # Independent numpy statement of the rule: the spouse whose own amount is + # greater takes the dependents' total; an exact tie splits it equally. + n_units = unit.max() + 1 + + def unit_sum(mask): + return np.bincount(unit, weights=own * mask, minlength=n_units) + + head_own = unit_sum(head)[unit] + spouse_own = unit_sum(spouse)[unit] + dependents_total = unit_sum(dependent)[unit] + has_spouse = np.bincount(unit, weights=spouse, minlength=n_units)[unit] > 0 + mine = np.where(head, head_own, spouse_own) + other = np.where(head, spouse_own, head_own) + share = np.where(mine > other, 1.0, np.where(mine == other, 0.5, 0.0)) + share = np.where(has_spouse, share, 1.0) * (head | spouse) + return np.where(dependent, 0.0, own) + share * dependents_total + + +def _check(units, year): + sim = Simulation(situation=_situation(units, year)) + states = np.array([u["state"] for u in units]) + + # 1. Swap invariance of each state's income tax. + for name in TAX_UNIT_OUTPUTS: + values = np.asarray(sim.calculate(name, year), dtype=float) + assert np.isfinite(values).all(), f"{name} in {year} is not finite" + drawn, swapped = values[0::2], values[1::2] + differs = ~(np.abs(swapped - drawn) <= TOLERANCE) + assert not differs.any(), ( + f"{name} in {year} changes when the head and spouse labels are " + f"exchanged: " + + ", ".join( + f"{states[i]} {drawn[i]:.2f} -> {swapped[i]:.2f}" + for i in np.flatnonzero(differs)[:10] + ) + ) + + # Tax units are numbered in the order _situation adds them: couple i's + # first copy is unit 2i and its second copy is unit 2i + 1. + unit = np.asarray(sim.populations["tax_unit"].members_entity_id) + head = np.asarray(sim.calculate("is_tax_unit_head", year), dtype=bool) + spouse = np.asarray(sim.calculate("is_tax_unit_spouse", year), dtype=bool) + dependent = np.asarray(sim.calculate("is_tax_unit_dependent", year), dtype=bool) + # People keep their order in both copies, so person k of a couple's first + # copy is person k of its second. + first_copy = unit % 2 == 0 + for column in COLUMNS: + values = np.asarray(sim.calculate(column, year), dtype=float) + assert np.isfinite(values).all(), f"{column} in {year} is not finite" + # 2. Each person's column is the same under either labelling. + np.testing.assert_allclose( + values[~first_copy], + values[first_copy], + rtol=0, + atol=TOLERANCE, + equal_nan=False, + err_msg=f"{column} in {year}", + ) + if column == "mt_agi_indiv": + continue + # 3. Differential against the numpy allocation, where the column + # applies (its state, and its years for Iowa). + own = _own_amounts(sim, year, column) + reference = _reference_allocation(own, unit, head, spouse, dependent) + in_state = states[unit // 2] == column[:2].upper() + np.testing.assert_allclose( + values[in_state], + reference[in_state], + rtol=FLOAT32_RTOL, + atol=TOLERANCE, + equal_nan=False, + err_msg=f"{column} in {year}", + ) + + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +@settings( + max_examples=12, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.sampled_from(YEARS), st.lists(couples(), min_size=10, max_size=40)) +def test_state_income_tax_does_not_depend_on_head_label(year, units): + _check(units, year) + + +@pytest.mark.parametrize("year", YEARS) +def test_seeded_population(year): + units = _seeded_couples(AFFECTED, per_state=20) + _check(units + _edge_cases(AFFECTED) + _review_cases(), year) + + +def test_every_state_under_current_law(): + # Simulating every state's earlier years makes this the costliest batch, + # so the guard outside the nine states runs for one year. + _check(_seeded_couples(ALL_STATES, per_state=3), 2026) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_agi_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_agi_indiv.yaml index 2feecfbb789..586850ff1bb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_agi_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_agi_indiv.yaml @@ -19,7 +19,7 @@ output: ar_agi_indiv: [900, 1_800] -- name: Dependent AGI gets attributed to the head, AGI capped at 0 +- name: Dependent AGI goes to the spouse with the greater AGI, AGI capped at 0 period: 2022 input: people: @@ -65,6 +65,71 @@ ar_capped_retirement_or_disability_benefits_exemption_person: 6_000 ar_agi_indiv: 50_000 +- name: Dependent AGI goes to the spouse with the greater AGI when that spouse is not the head + period: 2022 + input: + people: + person1: + ar_gross_income_indiv: 1_000 + ar_exemptions: 100 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + person2: + ar_gross_income_indiv: 100 + ar_exemptions: 200 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + person3: + ar_gross_income_indiv: 500 + ar_exemptions: 200 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: AR + output: + # The same couple as above with the head and spouse labels exchanged. + ar_agi_indiv: [1_200, 0, 0] + +- name: Spouses with equal AGI split the dependent AGI equally + period: 2022 + input: + people: + person1: + ar_gross_income_indiv: 1_000 + ar_exemptions: 0 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + person2: + ar_gross_income_indiv: 1_000 + ar_exemptions: 0 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + person3: + ar_gross_income_indiv: 300 + ar_exemptions: 0 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: AR + output: + ar_agi_indiv: [1_150, 1_150, 0] + - name: Only taxable disability benefits enter Arkansas gross income on line 18. period: 2024 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.yaml index 2fc49a71e63..50bd878a7e5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.yaml @@ -11,7 +11,7 @@ tax_units: tax_unit: members: [person1, person2] - de_cdcc: 200 + de_cdcc_potential: 200 households: household: members: [person1, person2] @@ -32,7 +32,7 @@ tax_units: tax_unit: members: [person1, person2] - de_cdcc: 200 + de_cdcc_potential: 200 households: household: members: [person1, person2] @@ -53,7 +53,7 @@ tax_units: tax_unit: members: [person1, person2] - de_cdcc: 0 + de_cdcc_potential: 0 households: household: members: [person1, person2] @@ -74,7 +74,7 @@ tax_units: tax_unit: members: [person1, person2] - de_cdcc: 200 + de_cdcc_potential: 200 households: household: members: [person1, person2] @@ -99,7 +99,7 @@ tax_units: tax_unit: members: [person1, person2, person3] - de_cdcc: 200 + de_cdcc_potential: 200 households: household: members: [person1, person2, person3] diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.yaml index 5964fe8cf76..99f172671c0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.yaml @@ -122,3 +122,69 @@ state_code: DE output: de_personal_credit_indv: [110, 0] + +- name: Filing-exception child's credit goes to the claimable head's column to minimize tax + period: 2025 + input: + people: + claimable: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + de_income_tax_before_non_refundable_credits_indv: 100 + other_filer: + age: 40 + is_tax_unit_head: false + is_tax_unit_spouse: true + de_income_tax_before_non_refundable_credits_indv: 200 + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [claimable, other_filer, child] + filing_status: JOINT + households: + household: + members: [claimable, other_filer, child] + state_code: DE + output: + # The claimable filer has no own credit but may claim a dependent under + # the filing exception. Allocating the child's $110 there leaves $90 + # total tax; allocating it to the other filer would leave $100. + de_personal_credit_indv: [110, 110, 0] + +- name: Filing-exception child's credit goes to the claimable spouse's column to minimize tax + period: 2025 + input: + people: + claimable: + age: 40 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + de_income_tax_before_non_refundable_credits_indv: 100 + other_filer: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + de_income_tax_before_non_refundable_credits_indv: 200 + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [claimable, other_filer, child] + filing_status: JOINT + households: + household: + members: [claimable, other_filer, child] + state_code: DE + output: + # The claimable filer has no own credit but may claim a dependent under + # the filing exception. Allocating the child's $110 there leaves $90 + # total tax; allocating it to the other filer would leave $100. + de_personal_credit_indv: [110, 110, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_agi_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_agi_indiv.yaml index 7884fd15c73..8ed4a71f642 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_agi_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_agi_indiv.yaml @@ -19,7 +19,7 @@ output: de_agi_indiv: [900, 1_800] -- name: Dependent AGI gets attributed to the head, AGI capped at 0 +- name: Dependent AGI goes to the spouse with the greater AGI, AGI capped at 0 period: 2022 input: people: @@ -44,3 +44,68 @@ state_code: DE output: de_agi_indiv: [1_200, 0, 0] + +- name: Dependent AGI goes to the spouse with the greater AGI when that spouse is not the head + period: 2022 + input: + people: + person1: + de_pre_exclusions_agi: 1_000 + de_elderly_or_disabled_income_exclusion_indiv: 100 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + person2: + de_pre_exclusions_agi: 100 + de_elderly_or_disabled_income_exclusion_indiv: 200 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + person3: + de_pre_exclusions_agi: 500 + de_elderly_or_disabled_income_exclusion_indiv: 200 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: DE + output: + # The same couple as above with the head and spouse labels exchanged. + de_agi_indiv: [1_200, 0, 0] + +- name: Spouses with equal AGI split the dependent AGI equally + period: 2022 + input: + people: + person1: + de_pre_exclusions_agi: 1_000 + de_elderly_or_disabled_income_exclusion_indiv: 0 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + person2: + de_pre_exclusions_agi: 1_000 + de_elderly_or_disabled_income_exclusion_indiv: 0 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + person3: + de_pre_exclusions_agi: 300 + de_elderly_or_disabled_income_exclusion_indiv: 0 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: DE + output: + de_agi_indiv: [1_150, 1_150, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_income_tax_before_refundable_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_income_tax_before_refundable_credits.yaml index 06e6887cef6..e87de799027 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_income_tax_before_refundable_credits.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_income_tax_before_refundable_credits.yaml @@ -18,7 +18,7 @@ output: de_income_tax_before_refundable_credits: 0 -- name: FS4 path - EITC to higher-income spouse +- name: FS4 path - EITC to higher-remaining-tax spouse period: 2025 input: people: @@ -34,17 +34,17 @@ tax_unit: members: [person1, person2] de_files_separately: true - de_non_refundable_eitc: 100 + de_non_refundable_eitc_potential: 100 households: household: members: [person1, person2] state_code: DE output: - # Spouse has higher taxable income; gets all $100 EITC + # Spouse has higher remaining tax; gets all $100 EITC # Head line33: $200, Spouse line33: $800 - $100 = $700 de_income_tax_before_refundable_credits: 900 -- name: FS4 path - EITC capped at higher-income column tax +- name: FS4 path - EITC capped at higher-remaining-tax column tax period: 2025 input: people: @@ -60,17 +60,17 @@ tax_unit: members: [person1, person2] de_files_separately: true - de_non_refundable_eitc: 200 + de_non_refundable_eitc_potential: 200 households: household: members: [person1, person2] state_code: DE output: - # Head has higher taxable income; gets all $200 EITC (capped at $500) + # Head has higher remaining tax; gets all $200 EITC (capped at $500) # Head: $500 - $200 = $300, Spouse: $30 (no EITC) de_income_tax_before_refundable_credits: 330 -- name: FS4 path - EITC exceeds higher-income column tax (waste) +- name: FS4 path - EITC exceeds higher-remaining-tax column tax (waste) period: 2025 input: people: @@ -86,18 +86,18 @@ tax_unit: members: [person1, person2] de_files_separately: true - de_non_refundable_eitc: 1_000 + de_non_refundable_eitc_potential: 1_000 households: household: members: [person1, person2] state_code: DE output: - # Head has higher taxable income; EITC ($1000) capped at head $200 + # Head has higher remaining tax; EITC ($1000) capped at head $200 # Excess $800 wasted (NOT redistributed to spouse). # Head: $200 - $200 = $0, Spouse: $100 (no EITC) de_income_tax_before_refundable_credits: 100 -- name: FS4 path - tied taxable incomes route EITC to head +- name: FS4 path - tied remaining taxes permit EITC in either column period: 2025 input: people: @@ -113,12 +113,12 @@ tax_unit: members: [person1, person2] de_files_separately: true - de_non_refundable_eitc: 100 + de_non_refundable_eitc_potential: 100 households: household: members: [person1, person2] state_code: DE output: - # On tie, head is treated as higher-income; gets all $100 EITC + # On a remaining-tax tie, either column can take the credit; gets all $100 EITC # Head: $500 - $100 = $400, Spouse: $500 de_income_tax_before_refundable_credits: 900 diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.yaml new file mode 100644 index 00000000000..55119e67eb8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.yaml @@ -0,0 +1,565 @@ +- name: Equal taxable incomes route the dependent credit and the EITC to minimise tax + period: 2023 + absolute_error_margin: 0.01 + input: + people: + older: + age: 60 + employment_income: 20_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + younger: + age: 45 + employment_income: 20_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + child: + age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [older, younger, child] + marital_units: + couple: + members: [older, younger] + child_unit: + members: [child] + households: + household: + members: [older, younger, child] + state_code: DE + output: + # EITC goes to the column with greater tax remaining after other credits. + # The dependent's $110 goes to the older spouse's column, which also has + # the aged credit, and the EITC reduces the other column: + # 255 + (475 - 419.32) = 310.68. Giving the dependent credit to the + # younger spouse would leave 365 in each column and a total of 365. + de_taxable_income_indv: [16_750, 16_750, 0] + de_personal_credit_indv: [220, 110, 0] + de_income_tax_after_non_refundable_credits_indv: [255, 475, 0] + de_non_refundable_eitc: 419.32 + de_income_tax_before_refundable_credits_separate: 310.68 + +- name: The result is the same when the head and spouse labels are exchanged + period: 2023 + absolute_error_margin: 0.01 + input: + people: + older: + age: 60 + employment_income: 20_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + younger: + age: 45 + employment_income: 20_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + child: + age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [older, younger, child] + marital_units: + couple: + members: [older, younger] + child_unit: + members: [child] + households: + household: + members: [older, younger, child] + state_code: DE + output: + de_taxable_income_indv: [16_750, 16_750, 0] + de_personal_credit_indv: [220, 110, 0] + de_income_tax_after_non_refundable_credits_indv: [255, 475, 0] + de_non_refundable_eitc: 419.32 + de_income_tax_before_refundable_credits_separate: 310.68 + +- name: Equal incomes, the dependent care credit and EITC columns are chosen together + period: 2023 + absolute_error_margin: 0.01 + input: + people: + younger: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 345 + de_aged_personal_credit_indv: 0 + older: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 345 + de_aged_personal_credit_indv: 110 + tax_units: + tax_unit: + members: [younger, older] + exemptions_count: 3 + de_cdcc_potential: 13.5 + de_non_refundable_eitc_potential: 250 + households: + household: + members: [younger, older] + state_code: DE + output: + # The dependent's $110 and the dependent care credit go to the older + # spouse's column (345 - 110 - 110 - 110 - 13.50 = 1.50) and the EITC + # clears the younger spouse's 345 - 110 = 235. Sending the dependent care + # credit to the column with more room instead leaves 15. + de_income_tax_before_refundable_credits_separate: 1.5 + +- name: Equal incomes, the same result when the older spouse is head + period: 2023 + absolute_error_margin: 0.01 + input: + people: + younger: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 345 + de_aged_personal_credit_indv: 0 + older: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 345 + de_aged_personal_credit_indv: 110 + tax_units: + tax_unit: + members: [younger, older] + exemptions_count: 3 + de_cdcc_potential: 13.5 + de_non_refundable_eitc_potential: 250 + households: + household: + members: [younger, older] + state_code: DE + output: + de_income_tax_before_refundable_credits_separate: 1.5 + +- name: Equal incomes, the dependent split minimises tax after the EITC + period: 2023 + absolute_error_margin: 0.01 + input: + people: + younger: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 300 + de_aged_personal_credit_indv: 0 + older: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 300 + de_aged_personal_credit_indv: 110 + tax_units: + tax_unit: + members: [younger, older] + exemptions_count: 3 + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 200 + households: + household: + members: [younger, older] + state_code: DE + output: + # The dependent's $110 goes to the older spouse's column, where the aged + # credit already uses most of the tax, so the EITC clears the younger + # spouse's 300 - 110 = 190. Maximising the personal credits used first + # would give the younger column the $110 and leave 80. + de_income_tax_before_refundable_credits_separate: 0 + +- name: Equal incomes, the dependent split minimises tax after the EITC when the older spouse is head + period: 2023 + absolute_error_margin: 0.01 + input: + people: + younger: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 300 + de_aged_personal_credit_indv: 0 + older: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 11_750 + de_income_tax_before_non_refundable_credits_indv: 300 + de_aged_personal_credit_indv: 110 + tax_units: + tax_unit: + members: [younger, older] + exemptions_count: 3 + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 200 + households: + household: + members: [younger, older] + state_code: DE + output: + de_income_tax_before_refundable_credits_separate: 0 + +# Review r1: $0.015625 swap difference from scoring credits at float64. +- name: High-income credit routing uses downstream precision with older spouse as + head + period: 2025 + absolute_error_margin: 0.001 + input: + people: + older: + age: 60 + employment_income: 1015250 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + younger: + age: 45 + employment_income: 1015250 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + child: + age: 10 + pre_subsidy_childcare_expenses: 190.2 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: + - older + - younger + - child + marital_units: + couple: + members: + - older + - younger + child_unit: + members: + - child + households: + household: + members: + - older + - younger + - child + state_code: DE + output: + de_income_tax_before_refundable_credits_separate: 131091.96875 + state_income_tax_before_refundable_credits: 131091.96875 + state_income_tax: 131091.96875 + +- name: High-income credit routing uses downstream precision with younger spouse as + head + period: 2025 + absolute_error_margin: 0.001 + input: + people: + older: + age: 60 + employment_income: 1015250 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + younger: + age: 45 + employment_income: 1015250 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + child: + age: 10 + pre_subsidy_childcare_expenses: 190.2 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: + - older + - younger + - child + marital_units: + couple: + members: + - older + - younger + child_unit: + members: + - child + households: + household: + members: + - older + - younger + - child + state_code: DE + output: + de_income_tax_before_refundable_credits_separate: 131091.96875 + state_income_tax_before_refundable_credits: 131091.96875 + state_income_tax: 131091.96875 + +# PIT-RES applies credits against each combined separate column's own tax. +# https://revenuefiles.delaware.gov/2025/PITForms_Instructions/Instructions/PIT-RES_Instructions_2025-01.pdf#page=10 +# Joint AGI offsets the spouse's loss: 20,000 - 10,000 - 6,500 = 3,500 +# taxable income; (3,500 - 2,000) * 2.2% = 33, exhausted by personal credits. +# Separate AGI clips the loss at zero: 20,000 - 3,250 gives 16,750 taxable +# income and 585 tax; 585 - 110 own credit - 50 EITC potential = 425. +# The joint path's applied EITC is zero. +- name: Combined separate EITC uses its own tax limit when spouse has a loss + period: 2025 + absolute_error_margin: 0.001 + input: + people: + earner: + age: 45 + de_pre_exclusions_agi: 20_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + loss_spouse: + age: 45 + de_pre_exclusions_agi: -10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + tax_units: + tax_unit: + members: [earner, loss_spouse] + filing_status: JOINT + exemptions_count: 2 + de_tax_unit_itemizes: false + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 50 + marital_units: + couple: + members: [earner, loss_spouse] + households: + household: + members: [earner, loss_spouse] + state_code: DE + output: + de_income_tax_before_non_refundable_credits_unit: 33 + de_personal_credit: 33 + de_non_refundable_eitc: 0 + de_taxable_income_indv: [16_750, 0] + de_income_tax_after_non_refundable_credits_indv: [475, 0] + de_income_tax_before_refundable_credits_separate: 425 + +- name: Combined separate EITC uses its own tax limit when loss spouse is head + period: 2025 + absolute_error_margin: 0.001 + input: + people: + earner: + age: 45 + de_pre_exclusions_agi: 20_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + loss_spouse: + age: 45 + de_pre_exclusions_agi: -10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + tax_units: + tax_unit: + members: [earner, loss_spouse] + filing_status: JOINT + exemptions_count: 2 + de_tax_unit_itemizes: false + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 50 + marital_units: + couple: + members: [earner, loss_spouse] + households: + household: + members: [earner, loss_spouse] + state_code: DE + output: + de_income_tax_before_non_refundable_credits_unit: 33 + de_personal_credit: 33 + de_non_refundable_eitc: 0 + de_taxable_income_indv: [16_750, 0] + de_income_tax_after_non_refundable_credits_indv: [475, 0] + de_income_tax_before_refundable_credits_separate: 425 + +# 30 Del. C. 1117(b) and PIT-RSS Schedule II line 12 compare tax remaining +# after other credits, not taxable income. Supplied gross taxes isolate routing. +# https://delcode.delaware.gov/title30/c011/sc02/index.html#1117 +# https://revenuefiles.delaware.gov/2025/PITForms_Instructions/PIT-RSS_2025-01_PaperInteractiveIPM.pdf#page=1 +# Older spouse: max(213 - 110 own - 110 aged, 0) = 0. +# Younger spouse: 174 - 110 = 64; 64 - 48.80 EITC = 15.20. +# With tied taxable incomes: 213 - 110 = 103; 103 - 48.80 = 54.20. +- name: Lower taxable income sends EITC to the younger spouse + period: 2025 + absolute_error_margin: 0.001 + input: + people: + older: + age: 60 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 8_750 + de_income_tax_before_non_refundable_credits_indv: 213 + younger: + age: 45 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 7750 + de_income_tax_before_non_refundable_credits_indv: 174 + tax_units: + tax_unit: + members: [older, younger] + filing_status: JOINT + exemptions_count: 2 + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 48.8 + marital_units: + couple: + members: [older, younger] + households: + household: + members: [older, younger] + state_code: DE + output: + de_income_tax_after_non_refundable_credits_indv: [0, 64] + de_income_tax_before_refundable_credits_separate: 15.2 + +- name: Lower taxable income sends EITC to the younger head + period: 2025 + absolute_error_margin: 0.001 + input: + people: + older: + age: 60 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 8_750 + de_income_tax_before_non_refundable_credits_indv: 213 + younger: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 7750 + de_income_tax_before_non_refundable_credits_indv: 174 + tax_units: + tax_unit: + members: [older, younger] + filing_status: JOINT + exemptions_count: 2 + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 48.8 + marital_units: + couple: + members: [older, younger] + households: + household: + members: [older, younger] + state_code: DE + output: + de_income_tax_after_non_refundable_credits_indv: [0, 64] + de_income_tax_before_refundable_credits_separate: 15.2 + +- name: Tied taxable incomes sends EITC to the younger spouse + period: 2025 + absolute_error_margin: 0.001 + input: + people: + older: + age: 60 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 8_750 + de_income_tax_before_non_refundable_credits_indv: 213 + younger: + age: 45 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 8750 + de_income_tax_before_non_refundable_credits_indv: 213 + tax_units: + tax_unit: + members: [older, younger] + filing_status: JOINT + exemptions_count: 2 + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 48.8 + marital_units: + couple: + members: [older, younger] + households: + household: + members: [older, younger] + state_code: DE + output: + de_income_tax_after_non_refundable_credits_indv: [0, 103] + de_income_tax_before_refundable_credits_separate: 54.2 + +- name: Tied taxable incomes sends EITC to the younger head + period: 2025 + absolute_error_margin: 0.001 + input: + people: + older: + age: 60 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + de_taxable_income_indv: 8_750 + de_income_tax_before_non_refundable_credits_indv: 213 + younger: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + de_taxable_income_indv: 8750 + de_income_tax_before_non_refundable_credits_indv: 213 + tax_units: + tax_unit: + members: [older, younger] + filing_status: JOINT + exemptions_count: 2 + de_cdcc_potential: 0 + de_non_refundable_eitc_potential: 48.8 + marital_units: + couple: + members: [older, younger] + households: + household: + members: [older, younger] + state_code: DE + output: + de_income_tax_after_non_refundable_credits_indv: [0, 103] + de_income_tax_before_refundable_credits_separate: 54.2 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_net_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_net_income.yaml index 48cca945a7b..92178c63871 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_net_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/tax/income/ia_net_income.yaml @@ -26,7 +26,7 @@ output: ia_net_income: [9_400, 0, 0] -- name: IA net income unit test 2 +- name: Spouses with equal net income split the dependent net income equally absolute_error_margin: 0.01 period: 2022 input: @@ -52,7 +52,76 @@ members: [person1, person2, person3] state_code: IA output: - ia_net_income: [8_600, 8_000, 0] + # The spouses' net incomes are equal, so they split the dependent's. + ia_net_income: [8_300, 8_300, 0] + +- name: Dependent net income goes to the spouse with the greater net income, here not the head + absolute_error_margin: 0.01 + period: 2021 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + age: 40 + ia_gross_income: 10_000 + ia_income_adjustments: 2_000 + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + age: 38 + ia_gross_income: 30_000 + person3: + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + age: 17 + ia_gross_income: 600 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + ia_net_income: [8_000, 30_600, 0] + +- name: Dependent net income stays with the same spouse when the head and spouse labels are exchanged + absolute_error_margin: 0.01 + period: 2021 + input: + people: + person1: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + age: 40 + ia_gross_income: 10_000 + ia_income_adjustments: 2_000 + person2: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + age: 38 + ia_gross_income: 30_000 + person3: + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + age: 17 + ia_gross_income: 600 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + ia_net_income: [8_000, 30_600, 0] - name: 2022 single filer with wages and traditional IRA distribution period: 2022 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.yaml index 29e734c53c9..011e58b51a4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.yaml @@ -49,3 +49,72 @@ state_code: MN output: mn_cdcc_dependent_count: 0 + +- name: An incapacitated head counts as the qualifying spouse on a joint return + absolute_error_margin: 0.01 + period: 2026 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + age: 45 + is_incapable_of_self_care: true + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + age: 45 + employment_income: 20_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MN + output: + mn_cdcc_dependent_count: 1 + +- name: Two incapacitated spouses count once + absolute_error_margin: 0.01 + period: 2026 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + age: 45 + is_incapable_of_self_care: true + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + age: 45 + is_incapable_of_self_care: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MN + output: + mn_cdcc_dependent_count: 1 + +- name: A single filer is not their own qualifying individual + absolute_error_margin: 0.01 + period: 2026 + input: + people: + person1: + is_tax_unit_head: true + age: 45 + is_incapable_of_self_care: true + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MN + output: + mn_cdcc_dependent_count: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.yaml index 93cb6458c08..a27b6c90b5f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.yaml @@ -92,7 +92,7 @@ output: mo_ptc_taxunit_eligible: true -- name: Survivor pathway is claimant-only, a qualifying spouse does not count +- name: A spouse aged 60 or older with their own survivor benefits can be the claimant period: 2022 absolute_error_margin: 0 input: @@ -110,6 +110,29 @@ members: [person1, person2] state_code: MO output: + # A married couple files one claim and chooses who is listed first as + # the claimant (RSMo 135.010(1): "a person or persons"). + mo_ptc_taxunit_eligible: true + +- name: Survivor pathway tests age and benefits on the same person + period: 2022 + absolute_error_margin: 0 + input: + people: + person1: + is_tax_unit_head: true + age: 62 + rent: 3_600 + person2: + is_tax_unit_spouse: true + age: 58 + social_security_survivors: 6_000 + households: + household: + members: [person1, person2] + state_code: MO + output: + # Neither spouse is both 60 or older and receiving survivor benefits. mo_ptc_taxunit_eligible: false - name: A child's survivor benefits do not qualify an aged head diff --git a/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.yaml b/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.yaml index 999c32009f1..8776ba67bff 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.yaml @@ -23,7 +23,7 @@ ms_agi_adjustments: 1_500 ms_agi: 38_500 -- name: A joint return without per-person amounts attributes the deduction once, to the head +- name: A joint return without per-person amounts attributes the deduction once, half to each spouse period: 2025 input: people: @@ -42,10 +42,11 @@ members: [head, spouse] state_code: MS output: - ms_health_savings_account_adjustment: [3_000, 0] - ms_agi_adjustments: [3_000, 0] - # 40,000 - 3,000 and 30,000: the spouse's income is not reduced again. - ms_agi: [37_000, 30_000] + # The account holder is unknown, so each spouse takes half, whichever is + # labelled head; the deduction is still taken once. + ms_health_savings_account_adjustment: [1_500, 1_500] + ms_agi_adjustments: [1_500, 1_500] + ms_agi: [38_500, 28_500] - name: The spouse whose account it is takes the deduction period: 2025 @@ -143,11 +144,13 @@ members: [head, spouse, child] state_code: MS output: - ms_health_savings_account_adjustment: [3_000, 0, 0] - ms_agi_adjustments: [3_000, 0, 0] - # The child's 5,000 reaches the head's Mississippi AGI in full: - # 40,000 - 3,000 + 5,000. - ms_agi: [42_000, 30_000, 0] + # The filers' 3,000 is split between the spouses; the child's own + # account is left out. + ms_health_savings_account_adjustment: [1_500, 1_500, 0] + ms_agi_adjustments: [1_500, 1_500, 0] + # The child's 5,000 reaches the Mississippi AGI of the spouse with the + # greater income in full: 40,000 - 1,500 + 5,000. + ms_agi: [43_500, 28_500, 0] - name: A married person filing a separate federal return takes their own deduction period: 2025 @@ -198,7 +201,7 @@ # 4.4% above $10,000: 21,700 x 0.044 and 8,700 x 0.044. ms_income_tax_before_credits_indiv: [954.8, 382.8] -- name: Without per-person amounts the head takes the deduction, even above the head's own income +- name: Without per-person amounts each spouse takes half, even above one spouse's own income period: 2025 input: people: @@ -217,13 +220,39 @@ members: [head, spouse] state_code: MS output: - # The model does not know whose account it is, so the head takes it, as - # on a joint return with one income column. The head's Mississippi AGI - # column goes negative (1,000 - 3,000 = -2,000). On a joint return the - # tax computation combines a negative column with the other one (Form - # 80-100 line 17, #9885), so the whole deduction is used. - ms_health_savings_account_adjustment: [3_000, 0] - ms_agi: [-2_000, 50_000] + # The model does not know whose account it is, so each spouse takes + # half, whichever is labelled head. The head's Mississippi AGI column + # goes negative (1,000 - 1,500 = -500). On a joint return the tax + # computation combines a negative column with the other one (Form 80-100 + # line 17, #9885), so the whole deduction is used. + ms_health_savings_account_adjustment: [1_500, 1_500] + ms_agi: [-500, 48_500] # Combined: 1,000 + 33,400 - 3,000 = 31,400 of taxable income in Column A; # (31,400 - 10,000) x 4.4% = 941.60. ms_income_tax_before_credits_unit: 941.6 + +- name: Without per-person amounts the split is the same when the head and spouse labels are exchanged + period: 2025 + input: + people: + first: + age: 40 + employment_income: 40_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + second: + age: 40 + employment_income: 30_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [first, second] + health_savings_account_ald: 3_000 + households: + household: + members: [first, second] + state_code: MS + output: + ms_health_savings_account_adjustment: [1_500, 1_500] + ms_agi: [38_500, 28_500] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml index 585f37ffda5..35ad15eefab 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml @@ -1,4 +1,4 @@ -- name: The dependent income is allocated to the head +- name: The dependent income goes to the spouse with the greater income, here not the head period: 2021 input: people: @@ -22,7 +22,104 @@ members: [person1, person2, person3] state_code: MS output: - ms_agi: [12_000, 10_000, 0] + # The spouse's $10,000 is greater than the head's $9,000. + ms_agi: [9_000, 13_000, 0] + +- name: The dependent income stays with the same spouse when the head and spouse labels are exchanged + period: 2021 + input: + people: + person1: + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + ms_agi_adjustments: 1_000 + person2: + employment_income: 12_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + ms_agi_adjustments: 2_000 + person3: + employment_income: 3_000 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + ms_agi_adjustments: 0 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MS + output: + ms_agi: [9_000, 13_000, 0] + +- name: Spouses with equal income split the dependent income equally + period: 2021 + input: + people: + person1: + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + ms_agi_adjustments: 0 + person2: + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + ms_agi_adjustments: 0 + person3: + employment_income: 3_000 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + ms_agi_adjustments: 0 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MS + output: + ms_agi: [11_500, 11_500, 0] + +- name: A dependent's negative income is not moved to a spouse + period: 2021 + input: + people: + person1: + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + ms_agi_adjustments: 0 + person2: + employment_income: 4_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + ms_agi_adjustments: 0 + person3: + employment_income: 1_000 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + ms_agi_adjustments: 3_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MS + output: + ms_agi: [10_000, 4_000, 0] - name: A negative amount is kept, so a joint return can combine it with the other column period: 2021 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.yaml index b7ccaccc8ba..f98fbf9e6ec 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.yaml @@ -107,3 +107,46 @@ state_code: MT output: mt_child_dependent_care_expense_deduction_qualifying_individual: [false, true] + +- name: An incapacitated head is the other spouse's qualifying individual + period: 2023 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + age: 45 + is_incapable_of_self_care: true + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + age: 45 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_child_dependent_care_expense_deduction_qualifying_individual: [true, false] + +- name: An incapacitated single filer is not their own qualifying individual + period: 2023 + input: + people: + person1: + is_tax_unit_head: true + age: 45 + is_incapable_of_self_care: true + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + mt_child_dependent_care_expense_deduction_qualifying_individual: [false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml index 23a5bc1ffd9..a84d12ac6cb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml @@ -75,6 +75,93 @@ output: mt_itemized_deductions_for_federal_itemization_joint: [2_212, 0, 0] +- name: A joint return deducts the mortgage and investment interest either spouse paid + period: 2021 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + deductible_mortgage_interest: 4_000 + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + deductible_mortgage_interest: 6_000 + investment_interest_expense: 500 + households: + household: + members: [person1, person2] + state_code: MT + tax_units: + tax_unit: + members: [person1, person2] + charitable_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + output: + mt_itemized_deductions_for_federal_itemization_joint: [10_500, 0] + +- name: The joint deduction is the same when the head and spouse labels are exchanged + period: 2021 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + deductible_mortgage_interest: 4_000 + person2: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + deductible_mortgage_interest: 6_000 + investment_interest_expense: 500 + households: + household: + members: [person1, person2] + state_code: MT + tax_units: + tax_unit: + members: [person1, person2] + charitable_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + output: + mt_itemized_deductions_for_federal_itemization_joint: [0, 10_500] + +- name: A dependent's own mortgage interest is not deducted on the parents' joint return + period: 2021 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + deductible_mortgage_interest: 4_000 + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + person3: + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + deductible_mortgage_interest: 50_000 + households: + household: + members: [person1, person2, person3] + state_code: MT + tax_units: + tax_unit: + members: [person1, person2, person3] + charitable_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + output: + mt_itemized_deductions_for_federal_itemization_joint: [4_000, 0, 0] + - name: Mortgage interest on post-2017 debt above the federal cap is not deductible period: 2022 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml index d63d546a8f9..d71674cb460 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml @@ -75,6 +75,93 @@ output: mt_itemized_deductions_joint: [2_212, 0, 0] +- name: A joint return deducts the mortgage and investment interest either spouse paid + period: 2021 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + deductible_mortgage_interest: 4_000 + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + deductible_mortgage_interest: 6_000 + investment_interest_expense: 500 + households: + household: + members: [person1, person2] + state_code: MT + tax_units: + tax_unit: + members: [person1, person2] + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + output: + mt_itemized_deductions_joint: [10_500, 0] + +- name: The joint deduction is the same when the head and spouse labels are exchanged + period: 2021 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + deductible_mortgage_interest: 4_000 + person2: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + deductible_mortgage_interest: 6_000 + investment_interest_expense: 500 + households: + household: + members: [person1, person2] + state_code: MT + tax_units: + tax_unit: + members: [person1, person2] + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + output: + mt_itemized_deductions_joint: [0, 10_500] + +- name: A dependent's own mortgage interest is not deducted on the parents' joint return + period: 2021 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + deductible_mortgage_interest: 4_000 + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + person3: + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + deductible_mortgage_interest: 50_000 + households: + household: + members: [person1, person2, person3] + state_code: MT + tax_units: + tax_unit: + members: [person1, person2, person3] + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + output: + mt_itemized_deductions_joint: [4_000, 0, 0] + - name: Mortgage interest on post-2017 debt above the federal cap is not deductible period: 2022 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_indiv.yaml index 377b0d7d460..157f8aa56a0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi_indiv.yaml @@ -1,4 +1,4 @@ -- name: The dependent incoome is allocated to the head +- name: The dependent income goes to the spouse with the greater income period: 2021 input: people: @@ -67,3 +67,86 @@ state_code: MT output: mt_agi_indiv: [9_200, 1_600, 0] + +- name: The dependent income goes to the spouse with the greater income, here not the head + period: 2021 + input: + people: + person1: + is_tax_unit_head: false + is_tax_unit_spouse: true + adjusted_gross_income_person: 10_000 + mt_additions: 500 + mt_subtractions: 3_000 + mt_taxable_social_security: 0 + taxable_social_security: 0 + is_tax_unit_dependent: false + person2: + is_tax_unit_head: true + is_tax_unit_spouse: false + adjusted_gross_income_person: 2_000 + mt_additions: 300 + mt_subtractions: 700 + mt_taxable_social_security: 0 + taxable_social_security: 0 + is_tax_unit_dependent: false + person3: + is_tax_unit_head: false + is_tax_unit_spouse: false + adjusted_gross_income_person: 2_000 + mt_additions: 300 + mt_subtractions: 700 + mt_taxable_social_security: 0 + taxable_social_security: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # The first test's couple with the head and spouse labels exchanged. + mt_agi_indiv: [9_100, 1_600, 0] + +- name: Spouses with equal income split the dependent income equally + period: 2021 + input: + people: + person1: + is_tax_unit_head: true + is_tax_unit_spouse: false + adjusted_gross_income_person: 5_000 + mt_additions: 0 + mt_subtractions: 0 + mt_taxable_social_security: 0 + taxable_social_security: 0 + is_tax_unit_dependent: false + person2: + is_tax_unit_head: false + is_tax_unit_spouse: true + adjusted_gross_income_person: 5_000 + mt_additions: 0 + mt_subtractions: 0 + mt_taxable_social_security: 0 + taxable_social_security: 0 + is_tax_unit_dependent: false + person3: + is_tax_unit_head: false + is_tax_unit_spouse: false + adjusted_gross_income_person: 0 + mt_additions: 1_000 + mt_subtractions: 0 + mt_taxable_social_security: 0 + taxable_social_security: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_agi_indiv: [5_500, 5_500, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ok/tax/income/ok_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/states/ok/tax/income/ok_ptc.yaml index 0350d505792..0f41f9ff6f9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ok/tax/income/ok_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ok/tax/income/ok_ptc.yaml @@ -106,3 +106,19 @@ # Excess = max(0, $300 - 0.01 * $9,000) = max(0, $300 - $90) = $210 # Credit = min($200, $210) = $200 ok_ptc: 200 + +- name: OK PTC - 2025 - a disabled spouse under 65 qualifies + period: 2025 + input: + age_head: 55 + age_spouse: 55 + head_is_disabled: false + spouse_is_disabled: true + ok_gross_income: 9_000 + real_estate_taxes: 400 + state_code: OK + output: + # Either spouse can claim as the disabled head of household, so the + # couple qualifies whichever of them is listed first. + # Excess tax: $400 - 1% * $9,000 = $310, capped at $200. + ok_ptc: 200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.yaml index 5cd1cdbd1f5..ef7a0ad09f3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.yaml @@ -159,3 +159,50 @@ state_code: OR output: or_wfhdc_employment_eligible: false + +- name: On a joint return the spouse's earnings count when the head has none + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_disabled: false + person2: + age: 45 + is_tax_unit_head: false + is_tax_unit_spouse: true + employment_income: 20_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OR + output: + or_wfhdc_employment_eligible: true + +- name: On a joint return a disabled spouse has earned income imputed + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_disabled: true + person2: + age: 45 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OR + output: + or_wfhdc_employment_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.yaml index 18bfad0703a..848c030a00b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.yaml @@ -357,3 +357,21 @@ state_code: OR output: or_wfhdc_has_qualified_individual_eligible: true + +- name: A disabled single filer is not their own qualifying individual + period: 2021 + input: + people: + person1: + age: 40 + is_disabled: true + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OR + output: + or_wfhdc_has_qualified_individual_eligible: false diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index 891bfdd10f9..a5022e0f76b 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -126,7 +126,13 @@ def tax_unit_non_dep_add(tax_unit, period, variables, include_dependents=()): return total -def person_share_of_tax_unit_amount(person, period, tax_unit_variable, person_variable): +def person_share_of_tax_unit_amount( + person, + period, + tax_unit_variable, + person_variable, + split_between_spouses=False, +): """ Attribute a tax unit's amount to its members. @@ -135,9 +141,11 @@ def person_share_of_tax_unit_amount(person, period, tax_unit_variable, person_va own amounts when those sum to the tax unit's amount. When the tax-unit amount differs, as when it is an input or a reform changes its formula, the head's and spouse's own amounts are scaled to sum to it; if they have - no own amounts, the head takes it. With non-negative own amounts and a - non-negative tax-unit amount, as for deductions, no share is negative. - Every tax unit is assumed to have a head. + no own amounts, the head takes it, or with `split_between_spouses` the + head and spouse take equal halves, which does not depend on which spouse + is labelled head. With non-negative own amounts and a non-negative + tax-unit amount, as for deductions, no share is negative. Every tax unit + is assumed to have a head. """ # The person-level projector returns tax-unit values for each member. tax_unit = person.tax_unit @@ -154,9 +162,13 @@ def person_share_of_tax_unit_amount(person, period, tax_unit_variable, person_va out=np.zeros_like(filers_own, dtype=float), where=has_own, ) - reconciled = own * scale + person("is_tax_unit_head", period) * where( - has_own, 0, amount - ) + is_head = person("is_tax_unit_head", period) + if split_between_spouses: + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + takes_unattributed = head_or_spouse / max_(tax_unit.sum(head_or_spouse), 1) + else: + takes_unattributed = is_head + reconciled = own * scale + takes_unattributed * where(has_own, 0, amount) filer_share = where(matches, own, reconciled) return where(filer, filer_share, own) diff --git a/policyengine_us/variables/gov/states/ar/tax/income/ar_agi_indiv.py b/policyengine_us/variables/gov/states/ar/tax/income/ar_agi_indiv.py index bd630daeb90..17a3135e45d 100644 --- a/policyengine_us/variables/gov/states/ar/tax/income/ar_agi_indiv.py +++ b/policyengine_us/variables/gov/states/ar/tax/income/ar_agi_indiv.py @@ -7,15 +7,23 @@ class ar_agi_indiv(Variable): label = "Arkansas adjusted gross income for each individual" unit = USD definition_period = YEAR - reference = "https://www.dfa.arkansas.gov/wp-content/uploads/2022_AR1000F_and_AR1000NR_Instructions.pdf#page=14" + reference = ( + "https://www.dfa.arkansas.gov/wp-content/uploads/2022_AR1000F_and_AR1000NR_Instructions.pdf#page=14", + # Filing Status 4: the primary's income in column A, the spouse's in B + "https://www.dfa.arkansas.gov/wp-content/uploads/2025_AR1000F_and_AR1000NR_Instructions.pdf#page=12", + # Borrowed for dependents' income: parents filing separately report a + # child's income on the return of the parent with the greater + # taxable income + "https://www.law.cornell.edu/uscode/text/26/1#g_5_B", + "https://www.irs.gov/instructions/i8814", + ) defined_for = StateCode.AR def formula(person, period, parameters): gross_income = person("ar_gross_income_indiv", period) income_exemptions = person("ar_exemptions", period) net_income = max_(gross_income - income_exemptions, 0) - # allocate any dependent gross income to tax unit head - is_dependent = person("is_tax_unit_dependent", period) - sum_dep_net_income = person.tax_unit.sum(is_dependent * net_income) - is_head = person("is_tax_unit_head", period) - return ~is_dependent * net_income + is_head * sum_dep_net_income + # Each spouse's column holds their own income. The dependents' income + # the model counts here goes on the column of the spouse with the + # greater income (a modelling convention; see the helper). + return move_dependent_amounts_to_filer(person, period, net_income) diff --git a/policyengine_us/variables/gov/states/ar/tax/income/deductions/itemized/ar_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/ar/tax/income/deductions/itemized/ar_itemized_deductions_indiv.py index c57d8fd8a8c..3184ebcbaf9 100644 --- a/policyengine_us/variables/gov/states/ar/tax/income/deductions/itemized/ar_itemized_deductions_indiv.py +++ b/policyengine_us/variables/gov/states/ar/tax/income/deductions/itemized/ar_itemized_deductions_indiv.py @@ -34,6 +34,6 @@ def formula(person, period, parameters): # Round prorate to the nearest percent and then divide by 100 prorate = np.round(prorate * 100) / 100 - # Dependents should always return 0 as their AGI is always - # attributed to the head of the tax unit in ar_agi + # Dependents should always return 0 as ar_agi_indiv puts their + # income on a spouse's column return unit_deds * prorate diff --git a/policyengine_us/variables/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.py b/policyengine_us/variables/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.py index 8e6293187a0..d3f8b70a2bb 100644 --- a/policyengine_us/variables/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.py +++ b/policyengine_us/variables/gov/states/de/tax/income/credits/cdcc/de_cdcc_indv.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.de.tax.income.de_combined_separate_credits import ( + de_combined_separate_credit_choices, +) class de_cdcc_indv(Variable): @@ -15,15 +18,14 @@ def formula(person, period, parameters): # the tax imposed on the spouse with the lower taxable # income reported on Line 23." Locked to the lower-income # spouse's column under combined separate filing. - # Tie-break: when taxable incomes are equal, head is treated - # as the higher-income spouse, so CDCC routes to spouse. + # The model permits either column on equal taxable incomes. This + # unresolved tie-election reading is documented with the authority + # search in de_combined_separate_credit_choices (rulespec-us#1607). is_head = person("is_tax_unit_head", period) is_spouse = person("is_tax_unit_spouse", period) - - person_taxable = person("de_taxable_income_indv", period) - head_taxable = person.tax_unit.sum(is_head * person_taxable) - spouse_taxable = person.tax_unit.sum(is_spouse * person_taxable) - head_higher = head_taxable >= spouse_taxable - - is_lower_income = (is_head & ~head_higher) | (is_spouse & head_higher) - return is_lower_income * person.tax_unit("de_cdcc", period) + _, cdcc_head, _ = de_combined_separate_credit_choices( + person.tax_unit, period, parameters + ) + takes = (is_head & cdcc_head) | (is_spouse & ~cdcc_head) + # Line 33 caps this pre-ordering amount at the recipient column's tax. + return takes * person.tax_unit("de_cdcc_potential", period) diff --git a/policyengine_us/variables/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.py b/policyengine_us/variables/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.py index 9701ad23833..e8e3ece0bbc 100644 --- a/policyengine_us/variables/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.py +++ b/policyengine_us/variables/gov/states/de/tax/income/credits/personal/de_personal_credit_indv.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.de.tax.income.de_combined_separate_credits import ( + de_combined_separate_credit_choices, +) class de_personal_credit_indv(Variable): @@ -16,64 +19,26 @@ def formula(person, period, parameters): # a married couple with no dependents enters "$110 in each column # if Filing Status 4" - each spouse's own credit belongs to their # own column, and only the DEPENDENT credits may be split between - # columns. We allocate the dependent increments to maximise total - # credits used (i.e. minimise tax); the taxpayer may choose any - # dependent split, so the optimal one is the tax-minimising one. + # columns. The taxpayer may choose any dependent split; it is chosen + # together with the Line 31 and Line 34 columns to minimise tax (see + # de_combined_separate_credit_choices). p = parameters(period).gov.states.de.tax.income.credits + credit_per = p.personal_credits.personal is_head = person("is_tax_unit_head", period) is_spouse = person("is_tax_unit_spouse", period) - is_head_or_spouse = is_head | is_spouse - - credit_per = p.personal_credits.personal - person_tax = person("de_income_tax_before_non_refundable_credits_indv", period) - head_tax = person.tax_unit.sum(is_head * person_tax) - spouse_tax = person.tax_unit.sum(is_spouse * person_tax) - - fixed = person("de_aged_personal_credit_indv", period) + person( - "de_cdcc_indv", period - ) - # Each column's own $110 personal credit is fixed to that column. 30 - # Del. C. 1110(b)(1) gives it "for each personal exemption to which - # such individual is entitled ... for federal income tax purposes", - # and a spouse who can be claimed as a dependent has none (IRC - # 151(d)(2)). + # A filer who can be claimed has no own federal exemption (IRC + # 151(d)(2)), hence no own $110 credit under 30 Del. C. 1110(b)(1). head_own = where( person.tax_unit("head_is_dependent_elsewhere", period), 0, credit_per ) spouse_own = where( person.tax_unit("spouse_is_dependent_elsewhere", period), 0, credit_per ) - head_fixed = person.tax_unit.sum(is_head * fixed) + head_own - spouse_fixed = person.tax_unit.sum(is_spouse * fixed) + spouse_own - - head_capacity = max_(head_tax - head_fixed, 0) - spouse_capacity = max_(spouse_tax - spouse_fixed, 0) - - total_units = person.tax_unit("exemptions_count", period) - # Dependent increments: exemptions beyond the spouses' own. own_units = person.tax_unit("head_spouse_count_not_dependent_elsewhere", period) - dep_units = max_(total_units - own_units, 0) - - # Optimal dependent split: try proportional floor and ceil, pick - # whichever maximises effective credits (min of alloc vs - # remaining capacity in each column). - total_capacity = head_capacity + spouse_capacity - ratio = where(total_capacity > 0, head_capacity / total_capacity, 0) - - n_low = np.floor(dep_units * ratio) - n_high = n_low + 1 - n_low = max_(min_(n_low, dep_units), 0) - n_high = max_(min_(n_high, dep_units), 0) - - eff_low = min_(n_low * credit_per, head_capacity) + min_( - (dep_units - n_low) * credit_per, spouse_capacity + dep_units = max_(person.tax_unit("exemptions_count", period) - own_units, 0) + head_dep, _, _ = de_combined_separate_credit_choices( + person.tax_unit, period, parameters ) - eff_high = min_(n_high * credit_per, head_capacity) + min_( - (dep_units - n_high) * credit_per, spouse_capacity - ) - - n_head_dep = where(eff_high > eff_low, n_high, n_low) - head_alloc = head_own + n_head_dep * credit_per - spouse_alloc = spouse_own + (dep_units - n_head_dep) * credit_per - - return is_head_or_spouse * where(is_head, head_alloc, spouse_alloc) + head_alloc = head_own + head_dep * credit_per + spouse_alloc = spouse_own + (dep_units - head_dep) * credit_per + return where(is_head, head_alloc, where(is_spouse, spouse_alloc, 0)) diff --git a/policyengine_us/variables/gov/states/de/tax/income/de_agi_indiv.py b/policyengine_us/variables/gov/states/de/tax/income/de_agi_indiv.py index 27788a5f357..0722df0ce53 100644 --- a/policyengine_us/variables/gov/states/de/tax/income/de_agi_indiv.py +++ b/policyengine_us/variables/gov/states/de/tax/income/de_agi_indiv.py @@ -7,6 +7,15 @@ class de_agi_indiv(Variable): label = "Delaware adjusted gross income for each individual when married filing separately" unit = USD definition_period = YEAR + reference = ( + # Filing Status 3 or 4: each spouse reports their own income + "https://revenuefiles.delaware.gov/2025/PITForms_Instructions/Instructions/PIT-RES_Instructions_2025-01.pdf#page=5", + # Borrowed for dependents' income: parents filing separately report a + # child's income on the return of the parent with the greater + # taxable income + "https://www.law.cornell.edu/uscode/text/26/1#g_5_B", + "https://www.irs.gov/instructions/i8814", + ) defined_for = StateCode.DE def formula(person, period, parameters): @@ -15,10 +24,8 @@ def formula(person, period, parameters): "de_elderly_or_disabled_income_exclusion_indiv", period ) net_income = max_(pre_exclusions_agi - indv_exclusions, 0) - # allocate any dependent gross income to tax unit head - is_dependent = person("is_tax_unit_dependent", period) - sum_dep_net_income = person.tax_unit.sum(is_dependent * net_income) - is_head = person("is_tax_unit_head", period) - head_spouse_net_income = ~is_dependent * net_income - head_allocated_dependent_net_income = is_head * sum_dep_net_income - return head_spouse_net_income + head_allocated_dependent_net_income + # On a combined separate return each spouse reports their own income. + # The dependents' income the model counts here goes on the return of + # the spouse with the greater income (a modelling convention; see the + # helper). + return move_dependent_amounts_to_filer(person, period, net_income) diff --git a/policyengine_us/variables/gov/states/de/tax/income/de_combined_separate_credits.py b/policyengine_us/variables/gov/states/de/tax/income/de_combined_separate_credits.py new file mode 100644 index 00000000000..d986015006d --- /dev/null +++ b/policyengine_us/variables/gov/states/de/tax/income/de_combined_separate_credits.py @@ -0,0 +1,112 @@ +from policyengine_us.model_api import * + + +def de_combined_separate_credit_choices(tax_unit, period, parameters): + """The taxpayer's choices for column credits on a combined separate return. + + On a Delaware combined separate return (Filing Status 4) the couple + decides three things that move credits between the two columns: + - how to split the dependents' $110 personal credits (PIT-RES Line 27a: + "split the total between Columns A and B in increments of $110"); + - which column takes the dependent care credit, which "may only be + applied against the tax imposed on the spouse with the lower taxable + income reported on Line 23" (Line 31); + - which column takes the non-refundable EITC: 30 Del. C. 1117(b) and + PIT-RSS Schedule II Line 12 select the spouse with greater tax remaining + after other credits. Compare those balances within each allocation. + The model permits either CDCC column on equal Line 23 incomes; this + tie-election reading is retained by Max (d1684), rather than established + explicitly in the instructions. Equal remaining taxes permit either EITC + recipient. + The dependent split and credit recipients are chosen together to minimise + the two columns' tax after the EITC. Each column keeps its own $110 personal + credit, if the filer cannot be claimed as a dependent elsewhere, and its + aged credit. + The search treats the spouses alike, so the minimum does not depend on + which spouse is labelled head. + + Returns tax-unit arrays: the number of dependent increments in the head's + column, whether the head's column takes the dependent care credit, and + whether the head's column takes the EITC. + """ + person = tax_unit.members + is_head = person("is_tax_unit_head", period) + is_spouse = person("is_tax_unit_spouse", period) + + def column(values): + return tax_unit.sum(is_head * values), tax_unit.sum(is_spouse * values) + + p = parameters(period).gov.states.de.tax.income.credits + credit_per = p.personal_credits.personal + head_tax, spouse_tax = column( + person("de_income_tax_before_non_refundable_credits_indv", period) + ) + head_aged, spouse_aged = column(person("de_aged_personal_credit_indv", period)) + head_taxable, spouse_taxable = column(person("de_taxable_income_indv", period)) + # The separate columns apply their own limits; pooled applied credits + # have already been capped against the joint-return computation. + cdcc = tax_unit("de_cdcc_potential", period) + eitc = tax_unit("de_non_refundable_eitc_potential", period) + head_own = where(tax_unit("head_is_dependent_elsewhere", period), 0, credit_per) + spouse_own = where(tax_unit("spouse_is_dependent_elsewhere", period), 0, credit_per) + own_units = tax_unit("head_spouse_count_not_dependent_elsewhere", period) + dep_units = max_(tax_unit("exemptions_count", period) - own_units, 0) + + # Max (d1684) retains the CDCC tie election (rulespec-us#1607) and selects + # the statutory EITC rule: 1117(b) and PIT-RSS Schedule II line 12 compare + # tax remaining before EITC, after each candidate's other credits. + # https://delcode.delaware.gov/title30/c011/sc02/index.html#1114 + # https://delcode.delaware.gov/title30/c011/sc02/index.html#1117 + # https://revenuefiles.delaware.gov/2025/PITForms_Instructions/Instructions/PIT-RES_Instructions_2025-01.pdf#page=10 + # https://revenuefiles.delaware.gov/2025/PITForms_Instructions/PIT-RSS_2025-01_PaperInteractiveIPM.pdf#page=1 + head_may_take_cdcc = head_taxable <= spouse_taxable + spouse_may_take_cdcc = spouse_taxable <= head_taxable + + def line33(tax, own, aged, dependent_units, care_credit): + # Match the stored float32 personal credit, then the float32 credit + # addition and Line 33 subtraction in the downstream person formulas. + # Scoring unrounded credits can select different final taxes after a + # label swap. Aggregating each stored column promotes it to float64. + personal = (own + dependent_units * credit_per).astype(np.float32) + credits = personal + aged.astype(np.float32) + care_credit + tax = tax.astype(np.float32) + return (tax - min_(credits, tax)).astype(np.float64) + + # Keep the first choice only when the downstream tax is exactly equal. + # Allocations may differ on ties, but their final liabilities must agree. + best_tax = np.full_like(head_tax, np.inf) + best_n = np.zeros_like(head_tax) + best_cdcc_head = np.zeros_like(head_tax, dtype=bool) + best_eitc_head = np.zeros_like(head_tax, dtype=bool) + for n in range(int(dep_units.max(initial=0)) + 1): + for cdcc_head in (False, True): + for eitc_head in (True, False): + head_line33 = line33(head_tax, head_own, head_aged, n, cdcc_head * cdcc) + spouse_line33 = line33( + spouse_tax, + spouse_own, + spouse_aged, + dep_units - n, + (not cdcc_head) * cdcc, + ) + allowed = ( + (n <= dep_units) + & where(cdcc_head, head_may_take_cdcc, spouse_may_take_cdcc) + & where( + eitc_head, + head_line33 >= spouse_line33, + spouse_line33 >= head_line33, + ) + ) + head_eitc = min_(eitc, head_line33) if eitc_head else 0 + spouse_eitc = min_(eitc, spouse_line33) if not eitc_head else 0 + total = ( + max_(head_line33 - head_eitc, 0) + + max_(spouse_line33 - spouse_eitc, 0) + ).astype(np.float32) + better = allowed & (total < best_tax) + best_tax = where(better, total, best_tax) + best_n = where(better, n, best_n) + best_cdcc_head = where(better, cdcc_head, best_cdcc_head) + best_eitc_head = where(better, eitc_head, best_eitc_head) + return best_n, best_cdcc_head, best_eitc_head diff --git a/policyengine_us/variables/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.py b/policyengine_us/variables/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.py index 4b611021fdb..9544671f981 100644 --- a/policyengine_us/variables/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.py +++ b/policyengine_us/variables/gov/states/de/tax/income/de_income_tax_before_refundable_credits_separate.py @@ -9,14 +9,18 @@ class de_income_tax_before_refundable_credits_separate(Variable): ) unit = USD definition_period = YEAR - reference = "https://revenuefiles.delaware.gov/2025/PITForms_Instructions/Instructions/PIT-RES_Instructions_2025-01.pdf#page=10" + # Section 1117(b): EITC belongs to the spouse with greater remaining tax. + reference = ( + "https://delcode.delaware.gov/title30/c011/sc02/index.html#1117", + "https://revenuefiles.delaware.gov/2025/PITForms_Instructions/PIT-RSS_2025-01_PaperInteractiveIPM.pdf#page=1", + ) defined_for = StateCode.DE def formula(tax_unit, period, parameters): # Combined separate (FS4) path: per-column non-refundable credits are # already applied in de_income_tax_after_non_refundable_credits_indv # (PIT-RES Line 33). This step applies Line 34 (non-refundable EITC) - # to the higher-income spouse's column, capped at that column's tax. + # to the column with greater remaining tax, capped at that column's tax. members = tax_unit.members is_head = members("is_tax_unit_head", period) is_spouse = members("is_tax_unit_spouse", period) @@ -25,14 +29,11 @@ def formula(tax_unit, period, parameters): head_line33 = tax_unit.sum(is_head * line33) spouse_line33 = tax_unit.sum(is_spouse * line33) - # Tie-break: when taxable incomes are equal, head is treated as the - # higher-income spouse, so EITC routes to head. - person_taxable = members("de_taxable_income_indv", period) - head_taxable = tax_unit.sum(is_head * person_taxable) - spouse_taxable = tax_unit.sum(is_spouse * person_taxable) - head_higher = head_taxable >= spouse_taxable + # Apply the same remaining-tax comparison used to score each candidate + # allocation. Either recipient gives the same tax on an exact tie. + head_higher = head_line33 >= spouse_line33 - eitc = tax_unit("de_non_refundable_eitc", period) + eitc = tax_unit("de_non_refundable_eitc_potential", period) head_eitc = where(head_higher, min_(eitc, head_line33), 0) spouse_eitc = where(~head_higher, min_(eitc, spouse_line33), 0) diff --git a/policyengine_us/variables/gov/states/ia/tax/income/net_income/ia_net_income.py b/policyengine_us/variables/gov/states/ia/tax/income/net_income/ia_net_income.py index 403a3ec3876..92215df662b 100644 --- a/policyengine_us/variables/gov/states/ia/tax/income/net_income/ia_net_income.py +++ b/policyengine_us/variables/gov/states/ia/tax/income/net_income/ia_net_income.py @@ -12,6 +12,13 @@ class ia_net_income(Variable): "https://revenue.iowa.gov/media/2650/download?inline", "https://revenue.iowa.gov/sites/default/files/2023-01/2022IA1040%2841001%29.pdf", "https://revenue.iowa.gov/media/2721/download?inline", + # Status 3: married filing separately on a combined return + "https://revenue.iowa.gov/media/2650/download?inline#page=7", + # Borrowed for dependents' income: parents filing separately report a + # child's income on the return of the parent with the greater + # taxable income + "https://www.law.cornell.edu/uscode/text/26/1#g_5_B", + "https://www.irs.gov/instructions/i8814", ) defined_for = StateCode.IA @@ -19,8 +26,7 @@ def formula(person, period, parameters): gross_income = person("ia_gross_income", period) income_adjustments = person("ia_income_adjustments", period) net_income = gross_income - income_adjustments - # allocate any dependent net_income to tax unit head - is_dependent = person("is_tax_unit_dependent", period) - sum_dep_net_income = person.tax_unit.sum(is_dependent * net_income) - is_head = person("is_tax_unit_head", period) - return ~is_dependent * net_income + is_head * sum_dep_net_income + # Each spouse's column holds their own income. The dependents' net + # income the model counts here goes on the column of the spouse with + # the greater net income (a modelling convention; see the helper). + return move_dependent_amounts_to_filer(person, period, net_income) diff --git a/policyengine_us/variables/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.py b/policyengine_us/variables/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.py index d24b60e56ec..0478f785e18 100644 --- a/policyengine_us/variables/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.py +++ b/policyengine_us/variables/gov/states/mn/tax/income/credits/cdcc/mn_cdcc_dependent_count.py @@ -10,6 +10,8 @@ class mn_cdcc_dependent_count(Variable): reference = ( "https://www.revenue.state.mn.us/sites/default/files/2023-02/m1cd_21.pdf", "https://www.revenue.state.mn.us/sites/default/files/2023-01/m1cd_22_0.pdf", + "https://www.revisor.mn.gov/statutes/cite/290.067", + "https://www.law.cornell.edu/uscode/text/26/21#b_1", ) defined_for = "mn_cdcc_eligible" @@ -20,8 +22,17 @@ def formula(tax_unit, period, parameters): # ... children age = person("age", period) qualifies_by_age = age < p.child_age - # ... disability - non_head = ~person("is_tax_unit_head", period) + # ... disability: a dependent, or the taxpayer's spouse, who cannot + # care for themselves (IRC 21(b)(1)(B)-(C)). On a joint return either + # spouse can be the other's qualifying spouse, and, as in the federal + # count_cdcc_eligible, the couple counts at most one such spouse. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) disabled = person("is_incapable_of_self_care", period) - qualifies_by_disability = non_head & disabled - return tax_unit.sum(qualifies_by_age | qualifies_by_disability) + qualifies_by_disability = ~head_or_spouse & disabled + has_spouse = tax_unit.any(person("is_tax_unit_spouse", period)) + qualifying_spouse = has_spouse & tax_unit.any( + head_or_spouse & disabled & ~qualifies_by_age + ) + return tax_unit.sum(qualifies_by_age | qualifies_by_disability) + ( + qualifying_spouse + ) diff --git a/policyengine_us/variables/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.py b/policyengine_us/variables/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.py index 6a09542d4c1..0277af16479 100644 --- a/policyengine_us/variables/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.py +++ b/policyengine_us/variables/gov/states/mo/tax/income/credits/property_tax/mo_ptc_taxunit_eligible.py @@ -30,16 +30,16 @@ def formula(tax_unit, period, parameters): ) # The surviving-spouse pathway applies to the claimant only: the # claimant must be 60 or older and have received surviving spouse - # Social Security benefits themselves. The model treats the tax - # unit head as the filer/claimant, so the same-person test runs on - # the head; the other three pathways are claimant-or-spouse. + # Social Security benefits themselves. RSMo 135.010(1) defines the + # claimant as "a person or persons", and a married couple claims + # jointly, so either spouse who meets both tests can be the + # claimant, whichever is listed first. Age and benefits are tested + # on the same person. person = tax_unit.members - head_survivor_benefits = tax_unit.sum( - person("social_security_survivors", period) - * person("is_tax_unit_head", period) - ) - aged_survivor = (age_head >= p.aged_survivor_min_age) & ( - head_survivor_benefits > 0 + aged_survivor = tax_unit.any( + person("is_tax_unit_head_or_spouse", period) + & (person("age", period) >= p.aged_survivor_min_age) + & (person("social_security_survivors", period) > 0) ) categorical = elderly | disabled | military_disabled | aged_survivor # RSMo 135.025 bases the credit on property taxes accrued and rent diff --git a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.py b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.py index db83be36356..a54cf964c12 100644 --- a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.py +++ b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_health_savings_account_adjustment.py @@ -15,8 +15,9 @@ class ms_health_savings_account_adjustment(Variable): "to the account holders, rather than repeated for every member. The " "head and spouse get their own health_savings_account_ald_person " "amounts, scaled to sum to the tax unit's deduction; without them, the " - "head takes it, as on a joint return with one income column. A tax unit " - "dependent has no deduction on this return." + "head and spouse each take half, whichever is labelled head, since the " + "account holder is unknown. A tax unit dependent has no deduction on " + "this return." ) definition_period = YEAR reference = ( @@ -34,6 +35,7 @@ def formula(person, period, parameters): period, "health_savings_account_ald", "health_savings_account_ald_person", + split_between_spouses=True, ) # The tax unit's deduction is the head's and spouse's. A dependent who # holds an account reports it on their own Mississippi return, not diff --git a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_health_insurance_adjustment.py b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_health_insurance_adjustment.py index b0de576e2e9..6af56b19082 100644 --- a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_health_insurance_adjustment.py +++ b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_health_insurance_adjustment.py @@ -11,7 +11,8 @@ class ms_self_employed_health_insurance_adjustment(Variable): "80-105 line 56). Mississippi reports each spouse's income and " "adjustments separately, so the tax unit's deduction is not repeated " "for every member. A tax-unit deduction that differs from the head's " - "and spouse's own amounts is shared in proportion to them." + "and spouse's own amounts is shared in proportion to them; without own amounts the head and " + "spouse each take half." ) definition_period = YEAR reference = "https://www.dor.ms.gov/sites/default/files/tax-forms/individual/80100251%202.pdf#page=12" @@ -23,4 +24,5 @@ def formula(person, period, parameters): period, "self_employed_health_insurance_ald", "self_employed_health_insurance_ald_person", + split_between_spouses=True, ) diff --git a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_retirement_adjustment.py b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_retirement_adjustment.py index 77ac9425c84..8e00fee0b9b 100644 --- a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_retirement_adjustment.py +++ b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_retirement_adjustment.py @@ -11,7 +11,8 @@ class ms_self_employed_retirement_adjustment(Variable): "deduction (Form 80-105 line 51). Mississippi reports each spouse's " "income and adjustments separately, so the tax unit's deduction is not " "repeated for every member. A tax-unit deduction that differs from the " - "head's and spouse's own amounts is shared in proportion to them." + "head's and spouse's own amounts is shared in proportion to them; without own amounts the head and " + "spouse each take half." ) definition_period = YEAR reference = "https://www.dor.ms.gov/sites/default/files/tax-forms/individual/80100251%202.pdf#page=12" @@ -23,4 +24,5 @@ def formula(person, period, parameters): period, "self_employed_pension_contribution_ald", "self_employed_pension_contribution_ald_person", + split_between_spouses=True, ) diff --git a/policyengine_us/variables/gov/states/ms/tax/income/ms_agi.py b/policyengine_us/variables/gov/states/ms/tax/income/ms_agi.py index 3ac23b4329a..8c0c6ff2188 100644 --- a/policyengine_us/variables/gov/states/ms/tax/income/ms_agi.py +++ b/policyengine_us/variables/gov/states/ms/tax/income/ms_agi.py @@ -10,6 +10,13 @@ class ms_agi(Variable): reference = ( "https://www.dor.ms.gov/sites/default/files/tax-forms/individual/80100221.pdf#page=14", "https://www.dor.ms.gov/sites/default/files/tax-forms/individual/80105228.pdf", # Line 66 + # Combined return: one spouse's income in Column A, the other's in B + "https://www.dor.ms.gov/sites/default/files/tax-forms/individual/80100221.pdf#page=5", + # Borrowed for dependents' income: parents filing separately report a + # child's income on the return of the parent with the greater + # taxable income + "https://www.law.cornell.edu/uscode/text/26/1#g_5_B", + "https://www.irs.gov/instructions/i8814", ) defined_for = StateCode.MS @@ -20,8 +27,11 @@ def formula(person, period, parameters): # A spouse's column can be negative on a joint or combined return; # the tax computation combines it with the other column. net_income = gross_income - adjustments - # Allocate income from dependents to tax unit head. + # Each spouse's column holds their own income. The dependents' + # positive income the model counts here goes on the column of the + # spouse with the greater income (a modelling convention; see the + # helper). is_dependent = person("is_tax_unit_dependent", period) - sum_dep_net_income = person.tax_unit.sum(is_dependent * max_(net_income, 0)) - is_head = person("is_tax_unit_head", period) - return ~is_dependent * net_income + is_head * sum_dep_net_income + return move_dependent_amounts_to_filer( + person, period, where(is_dependent, max_(net_income, 0), net_income) + ) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_indiv.py index 043651bfe56..9ca64d63be9 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/base/mt_agi_indiv.py @@ -7,6 +7,15 @@ class mt_agi_indiv(Variable): label = "Montana Adjusted Gross Income for each individual" unit = USD definition_period = YEAR + reference = ( + # Filing separately: each column is its own return + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=12", + # Borrowed for dependents' income: parents filing separately report a + # child's income on the return of the parent with the greater + # taxable income + "https://www.law.cornell.edu/uscode/text/26/1#g_5_B", + "https://www.irs.gov/instructions/i8814", + ) defined_for = "mt_married_filing_separately_on_same_return_eligible" def formula(person, period, parameters): @@ -37,9 +46,8 @@ def formula(person, period, parameters): # 2024 and after: no longer apply the social security adjustment tax_unit_mt_agi = reduced_agi - # in all years - # allocate any dependent net_income to tax unit head - is_dependent = person("is_tax_unit_dependent", period) - sum_dep_net_income = person.tax_unit.sum(is_dependent * tax_unit_mt_agi) - is_head = person("is_tax_unit_head", period) - return ~is_dependent * tax_unit_mt_agi + is_head * sum_dep_net_income + # The dependents' income the model counts here goes on the return of + # the spouse with the greater income (a modelling convention; see the + # helper). The instructions' Line 5 exclusion of a child's income + # reported on federal Form 8814 is not modelled. + return move_dependent_amounts_to_filer(person, period, tax_unit_mt_agi) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.py index b7b5cf4bb3a..3ce9c430a8c 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_qualifying_individual.py @@ -20,12 +20,16 @@ def formula(person, period, parameters): ).gov.states.mt.tax.income.deductions.child_dependent_care_expense age = person("age", period) dependent = person("is_tax_unit_dependent", period) - spouse = person("is_tax_unit_spouse", period) eligible_child = dependent & (age < p.age_limit) # MCA 15-30-2131(1)(c)(i)(B)-(C) waives the age limit for a # dependent unable to provide self-care because of physical or # mental illness and includes a spouse who is unable to provide - # self-care. + # self-care. Either married filer is the other's spouse, whichever + # is listed first. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + married = person.tax_unit.any(person("is_tax_unit_spouse", period)) incapable_of_self_care = person("is_incapable_of_self_care", period) - eligible_adult = (dependent | spouse) & incapable_of_self_care + eligible_adult = (dependent | (head_or_spouse & married)) & ( + incapable_of_self_care + ) return eligible_child | eligible_adult diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py index 86ab41904a5..1ed4e6fd3e0 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py @@ -26,16 +26,23 @@ class mt_itemized_deductions_for_federal_itemization_joint(Variable): def formula(person, period, parameters): charitable_deduction = person.tax_unit("charitable_deduction", period) investment_interest = person("investment_interest_expense", period) - # Montana allows the mortgage interest federal law allows, so interest - # on acquisition debt above the IRC 163(h)(3) caps is not deductible - # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). - mortgage_interest = person("deductible_mortgage_interest", period) + # A joint return deducts the interest either spouse paid, excluding + # dependents. Montana allows only mortgage interest federal law + # allows (MCA 15-30-2131(1)(a); Form 2 itemized deductions line 9). + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + mortgage_interest = person.tax_unit.sum( + head_or_spouse * person("deductible_mortgage_interest", period) + ) # The joint return includes the federal premium deduction for both # spouses, once, in the head's itemized deduction total. mortgage_insurance = person.tax_unit( "deductible_mortgage_insurance_premiums", period ) - interest_ded = investment_interest + mortgage_interest + mortgage_insurance + interest_ded = ( + person.tax_unit.sum(head_or_spouse * investment_interest) + + mortgage_interest + + mortgage_insurance + ) other_deductions = add( person.tax_unit, period, diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py index 58f0ceff504..120ddac9db1 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py @@ -26,16 +26,23 @@ class mt_itemized_deductions_joint(Variable): def formula(person, period, parameters): charitable_deduction = person.tax_unit("charitable_deduction", period) investment_interest = person("investment_interest_expense", period) - # Montana allows the mortgage interest federal law allows, so interest - # on acquisition debt above the IRC 163(h)(3) caps is not deductible - # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). - mortgage_interest = person("deductible_mortgage_interest", period) + # A joint return deducts the interest either spouse paid, excluding + # dependents. Montana allows only mortgage interest federal law + # allows (MCA 15-30-2131(1)(a); Form 2 itemized deductions line 9). + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + mortgage_interest = person.tax_unit.sum( + head_or_spouse * person("deductible_mortgage_interest", period) + ) # The joint return includes the federal premium deduction for both # spouses, once, in the head's itemized deduction total. mortgage_insurance = person.tax_unit( "deductible_mortgage_insurance_premiums", period ) - interest_ded = investment_interest + mortgage_interest + mortgage_insurance + interest_ded = ( + person.tax_unit.sum(head_or_spouse * investment_interest) + + mortgage_interest + + mortgage_insurance + ) other_deductions = add( person.tax_unit, period, diff --git a/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_ptc.py b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_ptc.py index 5c54da9362d..8340c327ae0 100644 --- a/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_ptc.py +++ b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_ptc.py @@ -10,6 +10,9 @@ class ok_ptc(Variable): reference = ( # 2025 Form 538-H (Property Tax Credit form) "https://oklahoma.gov/content/dam/ok/en/tax/documents/forms/individuals/current/538-H.pdf", + # 68 O.S. 2906: a person 65 or older or totally disabled who heads + # the household + "https://www.oscn.net/applications/oscn/DeliverDocument.asp?CiteID=93068", ) defined_for = StateCode.OK documentation = """ @@ -21,7 +24,7 @@ class ok_ptc(Variable): Eligibility requirements (must meet ALL): 1. Age requirement: Head OR spouse must be age 65 or older, OR - head must be totally disabled + head OR spouse must be totally disabled 2. Income requirement: Gross household income must be $12,000 or less 3. Must have paid real estate taxes on Oklahoma homestead @@ -58,8 +61,12 @@ def formula(tax_unit, period, parameters): # Step 1: Check age/disability eligibility elderly_head = tax_unit("age_head", period) >= p.age_minimum elderly_spouse = tax_unit("age_spouse", period) >= p.age_minimum + # The claimant is a totally disabled head of household: a person who + # owns or jointly owns and maintains the home. On a joint return + # either spouse can be that claimant, as either spouse's age counts. disabled_head = tax_unit("head_is_disabled", period) - unit_eligible = elderly_head | elderly_spouse | disabled_head + disabled_spouse = tax_unit("spouse_is_disabled", period) + unit_eligible = elderly_head | elderly_spouse | disabled_head | disabled_spouse # Step 2: Check income eligibility (gross income <= $12,000) income = tax_unit("ok_gross_income", period) income_eligible = income <= p.income_limit diff --git a/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.py b/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.py index c6149d80869..cee883dd4d5 100644 --- a/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.py +++ b/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_employment_eligible.py @@ -9,26 +9,32 @@ class or_wfhdc_employment_eligible(Variable): definition_period = YEAR reference = ( "https://www.oregon.gov/dor/forms/FormsPubs/schedule-or-wfhdc-inst_101-195-1_2022.pdf#pahe=1", + "https://www.oregon.gov/dor/forms/FormsPubs/schedule-or-wfhdc-inst_101-195-1_2025.pdf#page=1", "https://law.justia.com/codes/oregon/2021/volume-08/chapter-315/section-315-264/", ) defined_for = StateCode.OR def formula(tax_unit, period, parameters): person = tax_unit.members - # 1) you are working + # 1) you are working. On a joint return "you" also means your + # spouse, so either filer's earnings count. earned_income = person("earned_income", period) head = person("is_tax_unit_head", period) - earned_income_eligible = tax_unit.any(head * earned_income) > 0 + is_head_or_spouse = person("is_tax_unit_head_or_spouse", period) + earned_income_eligible = tax_unit.any(is_head_or_spouse & (earned_income > 0)) # 2) you are single and you attended school (full-time or part-time) filing_status = tax_unit("filing_status", period) attend_school = tax_unit.any(head & person("is_in_k12_school", period)) single_student = ( filing_status == filing_status.possible_values.SINGLE ) & attend_school - # 3) you are married filing jointly and one spouse attended school (full-time) or was disabled - is_head_or_spouse = person("is_tax_unit_head_or_spouse", period) + # 3) you are married filing jointly and one spouse attended school + # (full-time) or was disabled, so has earned income imputed is_full_time_student = person("is_full_time_student", period) - married_eligible = tax_unit.any(is_full_time_student & is_head_or_spouse) + disabled = person("is_disabled", period) + married_eligible = tax_unit.any( + (is_full_time_student | disabled) & is_head_or_spouse + ) joint_head_or_spouse_student = ( filing_status == filing_status.possible_values.JOINT ) & married_eligible diff --git a/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.py b/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.py index a66b7b7c66c..21b8275cb8f 100644 --- a/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.py +++ b/policyengine_us/variables/gov/states/or/tax/income/credits/wfhdc/or_wfhdc_has_qualified_individual_eligible.py @@ -9,6 +9,7 @@ class or_wfhdc_has_qualified_individual_eligible(Variable): definition_period = YEAR reference = ( "https://www.oregon.gov/dor/forms/FormsPubs/schedule-or-wfhdc-inst_101-195-1_2022.pdf#page=1", + "https://www.oregon.gov/dor/forms/FormsPubs/schedule-or-wfhdc-inst_101-195-1_2025.pdf#page=1", "https://law.justia.com/codes/oregon/2021/volume-08/chapter-315/section-315-264/", ) defined_for = StateCode.OR