From 1f0cd1b0f918fc3e51441ae08711e65a9ca8d181 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 11:03:59 -0400 Subject: [PATCH 01/32] Make head_is_dependent_elsewhere consumers independent of the head label The federal standard deduction and 17 state formulas read only whether the tax-unit head is claimed elsewhere, so a joint return where one spouse can be claimed was taxed differently depending on which spouse was labelled head. Each formula now applies its jurisdiction's rule for that case (either spouse, single filers only, per spouse, or only when every filer is claimed), using two new label-independent helpers. Adds swap-pair YAML cases and a Hypothesis property test. Co-Authored-By: Claude Opus 5.5 --- .../head-dependent-elsewhere-swap.fixed.md | 1 + .../itemized/threshold/dependent.yaml | 2 +- .../exemptions/dependent_on_other_return.yaml | 17 +- .../standard/dependent_elsewhere.yaml | 4 +- ...t_elsewhere_head_spouse_swap_invariance.py | 355 ++++++++++++++++++ .../standard/basic_standard_deduction.yaml | 56 +++ .../hi/tax/income/credits/act_163_sunset.yaml | 4 - .../food_excise/hi_food_excise_credit.yaml | 89 ++++- .../tax/income/credits/hi_act_115_rebate.yaml | 90 +++++ ...w_income_household_renters_tax_credit.yaml | 64 ++++ ...low_income_household_renters_eligible.yaml | 91 ++++- .../tax/income/deductions/hi_deductions.yaml | 134 ++++++- .../hi_salt_deduction_integration.yaml | 6 +- ...me_sales_tax_fairness_credit_eligible.yaml | 85 ++++- .../tax/income/exemptions/mi_exemptions.yaml | 148 +++++++- .../income/credits/wftc/mo_wftc_eligible.yaml | 56 +++ .../income/credits/cdcc/nm_cdcc_eligible.yaml | 105 +++++- .../credits/nm_medical_expense_credit.yaml | 92 ++++- ...ction_for_certain_dependents_eligible.yaml | 86 ++++- .../2021_income/nm_2021_income_rebate.yaml | 90 ++++- .../nm_additional_2021_income_rebate.yaml | 89 ++++- .../nm_supplemental_2021_income_rebate.yaml | 89 ++++- .../deductions/ny_standard_deduction.yaml | 94 +++++ .../standard/or_standard_deduction.yaml | 60 ++- .../va_low_income_tax_credit_eligible.yaml | 56 ++- ...head_or_spouse_is_dependent_elsewhere.yaml | 114 ++++++ ..._spouse_count_not_dependent_elsewhere.yaml | 114 ++++++ .../basic_standard_deduction.py | 14 +- .../food_excise/hi_food_excise_credit.py | 25 +- .../hi_food_excise_exemption_amount.py | 18 +- .../tax/income/credits/hi_act_115_rebate.py | 16 +- ...low_income_household_renters_tax_credit.py | 30 +- ...r_low_income_household_renters_eligible.py | 16 +- .../hi/tax/income/deductions/hi_deductions.py | 27 +- .../me_sales_tax_fairness_credit_eligible.py | 16 +- .../mi/tax/income/exemptions/mi_exemptions.py | 29 +- .../mo/tax/income/credits/mo_wftc_eligible.py | 18 +- .../income/credits/cdcc/nm_cdcc_eligible.py | 20 +- .../credits/nm_medical_expense_credit.py | 16 +- ...duction_for_certain_dependents_eligible.py | 16 +- .../2021_rebate/nm_2021_income_rebate.py | 17 +- .../nm_additional_2021_income_rebate.py | 17 +- .../nm_supplemental_2021_income_rebate.py | 17 +- .../deductions/ny_standard_deduction.py | 20 +- .../deductions/or_standard_deduction.py | 11 +- .../va_low_income_tax_credit_eligible.py | 17 +- .../head_or_spouse_is_dependent_elsewhere.py | 21 ++ ...ad_spouse_count_not_dependent_elsewhere.py | 20 + 48 files changed, 2413 insertions(+), 179 deletions(-) create mode 100644 changelog.d/head-dependent-elsewhere-swap.fixed.md create mode 100644 policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.yaml create mode 100644 policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.yaml create mode 100644 policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.yaml create mode 100644 policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py create mode 100644 policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py diff --git a/changelog.d/head-dependent-elsewhere-swap.fixed.md b/changelog.d/head-dependent-elsewhere-swap.fixed.md new file mode 100644 index 00000000000..4b4d878f15f --- /dev/null +++ b/changelog.d/head-dependent-elsewhere-swap.fixed.md @@ -0,0 +1 @@ +The federal standard deduction and the New York, Michigan, Oregon, Virginia, Missouri, New Mexico, Maine and Hawaii rules for a filer claimed as a dependent elsewhere no longer depend on which spouse is labelled head; each follows its jurisdiction's rule for a joint return where one spouse can be claimed. New York's $3,100 dependent standard deduction now applies only to single filers, and Hawaii now limits the standard deduction of a filer who can be claimed as a dependent. diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml index 0a894e4a148..3ff2ac9a451 100644 --- a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml @@ -1,4 +1,4 @@ -description: Hawaii allows for itemized deductions for dependent filers with deductions above this amount. +description: Hawaii limits the standard deduction of a filer who can be claimed as a dependent to the greater of this amount or earned income, and lets such a filer itemize deductions above that limit. values: 2018-01-01: 500 metadata: diff --git a/policyengine_us/parameters/gov/states/mi/tax/income/exemptions/dependent_on_other_return.yaml b/policyengine_us/parameters/gov/states/mi/tax/income/exemptions/dependent_on_other_return.yaml index 44fbabc57c8..46ca0dabbff 100644 --- a/policyengine_us/parameters/gov/states/mi/tax/income/exemptions/dependent_on_other_return.yaml +++ b/policyengine_us/parameters/gov/states/mi/tax/income/exemptions/dependent_on_other_return.yaml @@ -1,21 +1,10 @@ -description: Michigan provides this exemption amount for a taxpayer who can be claimed as a dependent on someone else’s return. -SINGLE: - 2021-01-01: 1_500 -JOINT: - 2021-01-01: 3_000 -HEAD_OF_HOUSEHOLD: - 2021-01-01: 1_500 -SURVIVING_SPOUSE: - 2021-01-01: 1_500 -SEPARATE: +description: Michigan provides this exemption amount to each individual who can be claimed as a dependent on another taxpayer's return, in place of that individual's personal exemption. +values: 2021-01-01: 1_500 metadata: - propagate_metadata_to_children: true unit: currency-USD period: year - breakdown: - - filing_status - label: Michigan dependent on other return exemption amount + label: Michigan exemption amount per individual claimable as a dependent elsewhere reference: - title: Michigan Legal Code Section 206.30 - (4) href: https://www.legislature.mi.gov/Laws/MCL?objectName=mcl-206-30 diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/standard/dependent_elsewhere.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/standard/dependent_elsewhere.yaml index 4744cadf89b..efe6e74704b 100644 --- a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/standard/dependent_elsewhere.yaml +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/standard/dependent_elsewhere.yaml @@ -1,10 +1,10 @@ -description: New York standard deduction for filers who can be claimed as a dependent elsewhere. +description: New York provides this standard deduction to single filers who can be claimed as a dependent on another taxpayer's federal return. values: 2020-01-01: 3_100 metadata: unit: currency-USD period: year - label: New York standard deduction for dependent filers + label: New York standard deduction for single dependent filers reference: - title: 2021 NY Form IT-201 Instructions Line 34 href: https://www.tax.ny.gov/pdf/2021/inc/it201i_2021.pdf#page=19 diff --git a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py new file mode 100644 index 00000000000..8c3e6fc2c48 --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py @@ -0,0 +1,355 @@ +"""Rules for a filer claimed as a dependent elsewhere do not depend on which +spouse is labelled head. + +A married couple may list either spouse first, so exchanging the two adults' +`is_tax_unit_head` and `is_tax_unit_spouse` labels, with every other input +kept, must not change their taxes. On main, the federal standard deduction and +seventeen state formulas read only `head_is_dependent_elsewhere`, so a joint +return where one spouse can be claimed on another return (a joint return filed +only to recover withholding can meet the federal exception, IRS Publication +501) was taxed differently depending on which spouse was labelled head. Each +formula now applies its jurisdiction's rule for that case: + +- Either spouse limits the return (the IRC 63(c)(5) worksheet): the federal + standard deduction, Oregon's and Hawaii's standard deductions, and + Virginia's credit for low-income individuals. +- Only single filers: New York's dependent standard deduction. +- Each spouse separately: Michigan's exemptions and Hawaii's food/excise, + renters and Act 115 credits count only the filers who cannot be claimed. +- Only when every filer is a dependent: New Mexico's rebates, credits and + dependents deduction, and Maine's sales tax fairness credit. +- Never on a joint return: Missouri's working family credit, since such a + couple files as married filing combined. + +Hypothesis draws batches of married couples, with and without dependents, in +the eight states with such a rule and one without, for 2021-2026, with either, +both or neither spouse claimed elsewhere. A seeded population and crafted +cases add breadth in every year, and two seeded couples plus the review case +in every state guard 2026. Each batch is one vectorized simulation. For each couple: + +1. Swap invariance: federal income tax, state income tax before and after + refundable credits, and each of the eighteen formulas are the same to the + cent (or the same boolean) under either labelling. +2. Monotonicity: marking another filer as claimed elsewhere never raises any + of the eighteen amounts or turns an eligibility on. +3. The helper identities: `head_or_spouse_is_dependent_elsewhere` equals + `head_is_dependent_elsewhere | spouse_is_dependent_elsewhere`, and + `head_spouse_count_not_dependent_elsewhere` plus the claimed filers equals + the number of filers. +""" + +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 +YEARS = [2021, 2022, 2023, 2024, 2025, 2026] +AFFECTED = ["HI", "ME", "MI", "MO", "NM", "NY", "OR", "VA", "CA"] +ALL_STATES = [s for s in STATES if s not in ("PR", "VI")] +TAX_UNIT_OUTPUTS = [ + "income_tax", + "state_income_tax", + "state_income_tax_before_refundable_credits", +] +# The formulas that read whether a filer is claimed elsewhere. +CONSUMERS = [ + "basic_standard_deduction", + "ny_standard_deduction", + "mi_exemptions", + "or_standard_deduction", + "va_low_income_tax_credit_eligible", + "mo_wftc_eligible", + "nm_2021_income_rebate", + "nm_additional_2021_income_rebate", + "nm_supplemental_2021_income_rebate", + "nm_deduction_for_certain_dependents_eligible", + "nm_medical_expense_credit", + "nm_cdcc_eligible", + "me_sales_tax_fairness_credit_eligible", + "hi_deductions", + "hi_act_115_rebate", + "hi_food_excise_credit", + "hi_tax_credit_for_low_income_household_renters_eligible", + "hi_tax_credit_for_low_income_household_renters", +] +CLAIM_PATTERNS = [(True, False), (False, True), (True, True), (False, False)] + + +def money(high): + return st.one_of(st.just(0.0), st.integers(1, high).map(float)) + + +@st.composite +def adults(draw): + # Low incomes reach the credits' limits; ages reach the 65+ rules. + return { + "age": draw(st.integers(18, 90)), + "is_blind": draw(st.booleans()), + "is_disabled": draw(st.booleans()), + "employment_income": draw(money(60_000)), + "self_employment_income": draw(money(20_000)), + "taxable_interest_income": draw(money(3_000)), + "social_security_retirement": draw(money(25_000)), + "real_estate_taxes": draw(money(6_000)), + "rent": draw(money(18_000)), + "charitable_cash_donations": draw(money(6_000)), + "other_medical_expenses": draw(money(40_000)), + } + + +@st.composite +def dependents(draw): + age = draw(st.integers(0, 23)) + return { + "age": age, + "is_full_time_student": age >= 18, + "employment_income": draw(money(10_000)), + "pre_subsidy_childcare_expenses": draw(money(8_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=2)), + "claimed": draw(st.sampled_from(CLAIM_PATTERNS)), + } + + +SEED = 20261008 + + +def _seeded_adult(rng): + def some(high, p): + return float(round(rng.uniform(1, 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(60_000, 0.7), + "self_employment_income": some(20_000, 0.15), + "taxable_interest_income": some(3_000, 0.3), + "social_security_retirement": some(25_000, 0.5 if age >= 62 else 0.03), + "real_estate_taxes": some(6_000, 0.3), + "rent": some(18_000, 0.5), + "charitable_cash_donations": some(6_000, 0.3), + "other_medical_expenses": some(40_000, 0.15), + } + + +def _seeded_couples(states, per_state): + rng = np.random.default_rng(SEED) + return [ + { + "state": state, + "adults": [_seeded_adult(rng), _seeded_adult(rng)], + "dependents": [ + {"age": int(rng.integers(0, 18))} + for _ in range(int(rng.choice([0, 0, 1, 2]))) + ], + "claimed": CLAIM_PATTERNS[int(rng.integers(0, 3))], + } + for state in states + for _ in range(per_state) + ] + + +def _crafted_cases(states, review_case_only=False): + # The review case (married adults aged 20 earning $8,000 each, one + # claimed by a parent), with and without a child, and older couples who + # reach the 65+ and renter rules. + young = {"age": 20, "employment_income": 8_000.0, "rent": 6_000.0} + old = { + "age": 70, + "social_security_retirement": 12_000.0, + "rent": 9_000.0, + "other_medical_expenses": 30_000.0, + } + cases = [] + for state in states: + for claimed in CLAIM_PATTERNS[:3]: + cases += [ + { + "state": state, + "adults": [young, young], + "dependents": [], + "claimed": claimed, + }, + ] + if review_case_only: + continue + cases += [ + { + "state": state, + "adults": [young, {"age": 22, "employment_income": 3_000.0}], + "dependents": [{"age": 2}], + "claimed": claimed, + }, + { + "state": state, + "adults": [old, {**old, "age": 66}], + "dependents": [], + "claimed": claimed, + }, + ] + return cases + + +def _situation(units, year, variants): + # Each unit appears once per variant: (head is the first adult, first + # adult claimed, second adult claimed). People keep their order and every + # other input in every copy. + people = {} + groups = { + "tax_units": {}, + "spm_units": {}, + "families": {}, + "marital_units": {}, + "households": {}, + } + for i, unit in enumerate(units): + for copy, (first_is_head, claim_a, claim_b) in enumerate(variants(unit)): + a, b = f"a_{i}_{copy}", f"b_{i}_{copy}" + for name, amounts, is_head, claimed in ( + (a, unit["adults"][0], first_is_head, claim_a), + (b, unit["adults"][1], not first_is_head, claim_b), + ): + people[name] = { + **amounts, + "is_tax_unit_head": is_head, + "is_tax_unit_spouse": not is_head, + "is_tax_unit_dependent": False, + "claimed_as_dependent_on_another_return": claimed, + } + 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["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 _swap_variants(unit): + claim_a, claim_b = unit["claimed"] + return [(True, claim_a, claim_b), (False, claim_a, claim_b)] + + +def _calc(sim, name, year): + return np.asarray(sim.calculate(name, year), dtype=float) + + +def _check_swap(units, year, outputs): + sim = Simulation(situation=_situation(units, year, _swap_variants)) + states = np.array([u["state"] for u in units]) + for name in outputs: + values = _calc(sim, name, year) + 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]} claimed={units[i]['claimed']} " + f"{drawn[i]:.2f} -> {swapped[i]:.2f}" + for i in np.flatnonzero(differs)[:10] + ) + ) + # 3. The helper identities. + either = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) + head = _calc(sim, "head_is_dependent_elsewhere", year) + spouse = _calc(sim, "spouse_is_dependent_elsewhere", year) + np.testing.assert_array_equal(either, np.maximum(head, spouse)) + independent = _calc(sim, "head_spouse_count_not_dependent_elsewhere", year) + np.testing.assert_array_equal(independent + head + spouse, 2) + + +def _monotone_variants(unit): + # Nobody, either adult, then both adults claimed; labels fixed. + return [ + (True, False, False), + (True, True, False), + (True, False, True), + (True, True, True), + ] + + +def _check_monotone(units, year): + sim = Simulation(situation=_situation(units, year, _monotone_variants)) + states = np.array([u["state"] for u in units]) + for name in CONSUMERS: + values = _calc(sim, name, year).reshape(-1, 4) + nobody, first, second, both = values.T + for more, fewer, label in ( + (first, nobody, "first adult"), + (second, nobody, "second adult"), + (both, first, "both after first"), + (both, second, "both after second"), + ): + rises = more > fewer + TOLERANCE + assert not rises.any(), ( + f"{name} in {year} rises when the {label} is claimed " + f"elsewhere: " + + ", ".join( + f"{states[i]} {fewer[i]:.2f} -> {more[i]:.2f}" + for i in np.flatnonzero(rises)[:10] + ) + ) + + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +@settings( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.sampled_from(YEARS), st.lists(couples(), min_size=8, max_size=20)) +def test_claimed_spouse_does_not_depend_on_head_label(year, units): + _check_swap(units, year, TAX_UNIT_OUTPUTS + CONSUMERS) + + +@pytest.mark.parametrize("year", YEARS) +def test_seeded_population(year): + units = _seeded_couples(AFFECTED, per_state=4) + _crafted_cases(AFFECTED) + _check_swap(units, year, TAX_UNIT_OUTPUTS + CONSUMERS) + _check_monotone(units, year) + + +def test_every_state_with_a_claimed_spouse(): + # Other states' rules for a claimed filer read both spouses already; + # this guards their state income tax in one year. + units = _seeded_couples(ALL_STATES, per_state=2) + _crafted_cases( + ALL_STATES, review_case_only=True + ) + _check_swap(units, 2026, TAX_UNIT_OUTPUTS) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml index 483f073599a..b9a821293a5 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml @@ -207,3 +207,59 @@ members: [head] output: basic_standard_deduction: 3_650 + +- name: 2025 joint return uses the dependent worksheet when either spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: CA + output: + # Joint earned income $16,000 + $450 = $16,450, below the $31,500 joint amount. + basic_standard_deduction: 16_450 + +- name: 2025 joint return uses the dependent worksheet when either spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: CA + output: + # Joint earned income $16,000 + $450 = $16,450, below the $31,500 joint amount. + basic_standard_deduction: 16_450 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/act_163_sunset.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/act_163_sunset.yaml index 4116a0a6dac..778252cac05 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/act_163_sunset.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/act_163_sunset.yaml @@ -34,7 +34,6 @@ adjusted_gross_income: 10_000 filing_status: JOINT exemptions_count: 2 - head_is_dependent_elsewhere: false households: household: members: [person1, person2] @@ -58,7 +57,6 @@ adjusted_gross_income: 4_000 filing_status: JOINT exemptions_count: 2 - head_is_dependent_elsewhere: false households: household: members: [person1, person2] @@ -80,7 +78,6 @@ adjusted_gross_income: 10_000 filing_status: SINGLE exemptions_count: 1 - head_is_dependent_elsewhere: false households: household: members: [person1] @@ -102,7 +99,6 @@ adjusted_gross_income: 4_000 filing_status: SINGLE exemptions_count: 1 - head_is_dependent_elsewhere: false households: household: members: [person1] diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml index 9ad568e7fee..e340975fe5c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml @@ -20,7 +20,6 @@ adjusted_gross_income: 4_000 filing_status: SINGLE exemptions_count: 3 - head_is_dependent_elsewhere: false households: household: members: [person1, person2, person3] @@ -50,7 +49,6 @@ adjusted_gross_income: 4_000 filing_status: SINGLE exemptions_count: 3 - head_is_dependent_elsewhere: false households: household: members: [person1, person2, person3] @@ -71,6 +69,7 @@ person2: age: 39 is_child: false + claimed_as_dependent_on_another_return: true spm_units: spm_unit: members: [person1, person2] @@ -81,7 +80,6 @@ adjusted_gross_income: 4_000 filing_status: SINGLE exemptions_count: 2 - head_is_dependent_elsewhere: true households: household: members: [person1, person2] @@ -111,7 +109,6 @@ adjusted_gross_income: 35_000 filing_status: SINGLE exemptions_count: 3 - head_is_dependent_elsewhere: false households: household: members: [person1, person2, person3] @@ -141,7 +138,6 @@ adjusted_gross_income: 35_000 filing_status: JOINT exemptions_count: 3 - head_is_dependent_elsewhere: false households: household: members: [person1, person2, person3] @@ -171,7 +167,6 @@ adjusted_gross_income: 55_000 filing_status: JOINT exemptions_count: 3 - head_is_dependent_elsewhere: false households: household: members: [person1, person2, person3] @@ -197,7 +192,6 @@ members: [person1, person2] adjusted_gross_income: 6_000 filing_status: SINGLE - head_is_dependent_elsewhere: false households: household: members: [person1, person2] @@ -223,7 +217,6 @@ members: [person1, person2] adjusted_gross_income: 6_000 filing_status: SINGLE - head_is_dependent_elsewhere: false households: household: members: [person1, person2] @@ -258,3 +251,83 @@ exemptions_count: 2 hi_food_excise_credit: 440 hi_income_tax: -440 + +- name: 2027 joint return where only one spouse can be claimed (the claimable spouse is the head) + period: 2027 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + adjusted_gross_income: 10_000 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + # Form N-311 counts only the spouse who cannot be claimed: 1 x $220. + hi_food_excise_credit: 220 + +- name: 2027 joint return where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2027 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + adjusted_gross_income: 10_000 + households: + household: + members: [person_b, person_a] + state_code: HI + output: + # Form N-311 counts only the spouse who cannot be claimed: 1 x $220. + hi_food_excise_credit: 220 + +- name: 2027 joint return where both spouses can be claimed + period: 2027 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + adjusted_gross_income: 10_000 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + hi_food_excise_credit: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml index 13ebfd8d94a..7c4030e8e4c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml @@ -162,3 +162,93 @@ state_code: HI output: hi_act_115_rebate: 0 + +- name: Joint return with a child where only one spouse can be claimed (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 5_000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + # Only the spouse who cannot be claimed has a qualified exemption; a joint + # return on which a spouse can be claimed claims no dependents. 1 x $300. + hi_act_115_rebate: 300 + +- name: Joint return with a child where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 5_000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + # Only the spouse who cannot be claimed has a qualified exemption; a joint + # return on which a spouse can be claimed claims no dependents. 1 x $300. + hi_act_115_rebate: 300 + +- name: Joint return where both spouses can be claimed + period: 2021 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + adjusted_gross_income: 5_000 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + hi_act_115_rebate: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml index cddb436375d..2dd4cd51e42 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml @@ -37,3 +37,67 @@ hi_tax_credit_for_low_income_household_renters_eligible: false output: hi_tax_credit_for_low_income_household_renters: 0 + +- name: Joint return with a child where only one spouse can be claimed (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 66 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 66 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + # Schedule X lines 8-10: the spouse who cannot be claimed, plus their age + # 65 exemption. 2 x $50. + hi_tax_credit_for_low_income_household_renters: 100 + +- name: Joint return with a child where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 66 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 66 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + # Schedule X lines 8-10: the spouse who cannot be claimed, plus their age + # 65 exemption. 2 x $50. + hi_tax_credit_for_low_income_household_renters: 100 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.yaml index 4916c7667b8..629efc27e05 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.yaml @@ -2,7 +2,7 @@ period: 2021 input: state_code: HI - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false hi_agi: 29_000 rent: 1_100 output: @@ -12,7 +12,7 @@ period: 2021 input: state_code: HI - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false hi_agi: 31_000 rent: 1_100 output: @@ -22,7 +22,7 @@ period: 2021 input: state_code: HI - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false hi_agi: 29_000 rent: 900 output: @@ -32,8 +32,91 @@ period: 2021 input: state_code: HI - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false hi_agi: 32_000 rent: 900 output: hi_tax_credit_for_low_income_household_renters_eligible: false + +- name: Joint return where only one spouse can be claimed (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + rent: 1_100 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + hi_agi: 29_000 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + # HRS 235-55.7(b): the spouse who cannot be claimed may claim the credit. + hi_tax_credit_for_low_income_household_renters_eligible: true + +- name: Joint return where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 20 + rent: 1_100 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + hi_agi: 29_000 + households: + household: + members: [person_b, person_a] + state_code: HI + output: + # HRS 235-55.7(b): the spouse who cannot be claimed may claim the credit. + hi_tax_credit_for_low_income_household_renters_eligible: true + +- name: Joint return where both spouses can be claimed + period: 2021 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + rent: 1_100 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + hi_agi: 29_000 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + hi_tax_credit_for_low_income_household_renters_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml index fee63c3b063..377d8edd769 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml @@ -7,7 +7,7 @@ hi_agi: 1_000 hi_total_itemized_deductions: 5_000 hi_reduced_itemized_deductions: 1_000 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: hi_deductions: 5_000 @@ -20,7 +20,7 @@ hi_agi: 1_000_000 hi_total_itemized_deductions: 5_000 hi_reduced_itemized_deductions: 1_000 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: hi_deductions: 4_400 @@ -33,7 +33,7 @@ hi_agi: 1_000 hi_total_itemized_deductions: 1_000 hi_reduced_itemized_deductions: 1_000 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: hi_deductions: 4_400 @@ -46,7 +46,7 @@ hi_agi: 1_000_000 hi_total_itemized_deductions: 5_000 hi_reduced_itemized_deductions: 1_000 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: hi_deductions: 2_200 @@ -59,7 +59,7 @@ hi_agi: 1_000_000 hi_total_itemized_deductions: 5_000 hi_reduced_itemized_deductions: 4_500 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: hi_deductions: 4_500 @@ -71,3 +71,127 @@ employment_income: 62_000 output: hi_deductions: 8_800 + +- name: 2025 joint standard deduction limited when either spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + tax_unit_earned_income: 3_000 + hi_itemized_deductions: 0 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + # HRS 235-2.4(a)(3): greater of $500 or earned income ($3,000), up to $8,800. + hi_deductions: 3_000 + +- name: 2025 joint standard deduction limited when either spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + tax_unit_earned_income: 3_000 + hi_itemized_deductions: 0 + households: + household: + members: [person_b, person_a] + state_code: HI + output: + # HRS 235-2.4(a)(3): greater of $500 or earned income ($3,000), up to $8,800. + hi_deductions: 3_000 + +- name: 2025 joint filers with a claimable spouse itemize above the limited standard deduction (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + tax_unit_earned_income: 3_000 + hi_itemized_deductions: 4_000 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + # Itemized deductions of $4,000 exceed the limited $3,000 standard deduction. + hi_deductions: 4_000 + +- name: 2025 joint filers with a claimable spouse itemize above the limited standard deduction (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + tax_unit_earned_income: 3_000 + hi_itemized_deductions: 4_000 + households: + household: + members: [person_b, person_a] + state_code: HI + output: + # Itemized deductions of $4,000 exceed the limited $3,000 standard deduction. + hi_deductions: 4_000 + +- name: 2025 single dependent filer with little earned income gets the $500 minimum + period: 2025 + input: + state_code: HI + filing_status: SINGLE + claimed_as_dependent_on_another_return: true + tax_unit_earned_income: 300 + hi_itemized_deductions: 0 + output: + # HRS 235-2.4(a)(3): greater of $500 or earned income ($300). + hi_deductions: 500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_salt_deduction_integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_salt_deduction_integration.yaml index 74db648797c..7efb22ba1f0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_salt_deduction_integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_salt_deduction_integration.yaml @@ -4,7 +4,7 @@ state_code: HI filing_status: SINGLE age: 40 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false adjusted_gross_income: 99_999 hi_agi: 99_999 state_withheld_income_tax: 6_000 @@ -32,7 +32,7 @@ state_code: HI filing_status: SINGLE age: 40 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false adjusted_gross_income: 100_000 hi_agi: 100_000 state_withheld_income_tax: 6_000 @@ -60,7 +60,7 @@ state_code: HI filing_status: SINGLE age: 40 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false adjusted_gross_income: 99_999 hi_agi: 99_999 state_withheld_income_tax: 2_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.yaml index b3afa273542..b81ac606775 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.yaml @@ -2,7 +2,7 @@ period: 2022 input: state_code: ME - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true filing_status: JOINT output: me_sales_tax_fairness_credit_eligible: false @@ -11,7 +11,7 @@ period: 2022 input: state_code: ME - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false filing_status: SINGLE output: me_sales_tax_fairness_credit_eligible: true @@ -20,7 +20,86 @@ period: 2022 input: state_code: ME - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true filing_status: SEPARATE output: me_sales_tax_fairness_credit_eligible: false + +- name: Joint return where only one spouse can be claimed as a dependent (the claimable spouse is the head) + period: 2022 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: ME + output: + # 36 MRSA 5213-A(6)(C) excludes the claimable individual; the other spouse + # still qualifies for the joint credit. + me_sales_tax_fairness_credit_eligible: true + +- name: Joint return where only one spouse can be claimed as a dependent (the claimable spouse is the spouse) + period: 2022 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: ME + output: + # 36 MRSA 5213-A(6)(C) excludes the claimable individual; the other spouse + # still qualifies for the joint credit. + me_sales_tax_fairness_credit_eligible: true + +- name: Joint return where both spouses can be claimed as dependents + period: 2022 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: ME + output: + me_sales_tax_fairness_credit_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml index f916a47b51d..eee9da8f35d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml @@ -55,20 +55,36 @@ period: 2021 input: mi_personal_exemptions: 5_000 - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true filing_status: SINGLE state_code: MI output: mi_exemptions: 1_500 -- name: not disabled, is a dependent, married +- name: not disabled, both spouses can be claimed as dependents, married period: 2021 input: - mi_personal_exemptions: 10_000 - head_is_dependent_elsewhere: true - filing_status: JOINT - state_code: MI + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: MI output: + # $1,500 for each spouse. mi_exemptions: 3_000 # 2025 test cases - personal exemption $5,800, disabled exemption $3,400 @@ -301,15 +317,131 @@ person1: mi_disabled_exemption_eligible_person: true is_fully_disabled_service_connected_veteran: false + claimed_as_dependent_on_another_return: true tax_units: tax_unit: mi_personal_exemptions: 5_800 - head_is_dependent_elsewhere: true filing_status: SINGLE members: [person1] household: members: [person1] state_code: MI output: - # Dependent exemption: $1,500 (for single) + $3,400 disabled = $4,900 + # Dependent exemption: $1,500 + $3,400 disabled = $4,900 mi_exemptions: 4_900 + +- name: 2025 joint return where only one spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: MI + output: + # MCL 206.30(4): $1,500 for the claimable spouse, plus the other + # spouse's own $5,800 personal exemption. + mi_exemptions: 7_300 + +- name: 2025 joint return where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: MI + output: + # MCL 206.30(4): $1,500 for the claimable spouse, plus the other + # spouse's own $5,800 personal exemption. + mi_exemptions: 7_300 + +- name: 2025 joint return with a child where only one spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child_1] + state_code: MI + output: + # A joint return on which a spouse can be claimed claims no dependents + # (IRS Publication 501), so the child adds no exemption. + mi_exemptions: 7_300 + +- name: 2025 joint return with a child where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child_1] + state_code: MI + output: + # A joint return on which a spouse can be claimed claims no dependents + # (IRS Publication 501), so the child adds no exemption. + mi_exemptions: 7_300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml index dba14ed3fb0..6cb8a3e48c6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml @@ -133,3 +133,59 @@ # $4,401 is below the federal EITC investment income limit ($11,950 in # 2025) but above Missouri's $4,400 limit. mo_wftc_eligible: false + +- name: 2025 married couple filing combined is not stopped by a claimable spouse (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + eitc: 500 + households: + household: + members: [person_a, person_b] + state_code: MO + output: + # A joint federal filer whose spouse can be claimed still files Form MO-1040 + # as married filing combined, not as claimed as a dependent. + mo_wftc_eligible: true + +- name: 2025 married couple filing combined is not stopped by a claimable spouse (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + eitc: 500 + households: + household: + members: [person_b, person_a] + state_code: MO + output: + # A joint federal filer whose spouse can be claimed still files Form MO-1040 + # as married filing combined, not as claimed as a dependent. + mo_wftc_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.yaml index df774c30456..de3db5f2233 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.yaml @@ -1,7 +1,7 @@ - name: Is dependent on another return period: 2022 input: - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true state_code: NM earned_income: 1 is_disabled: false @@ -15,7 +15,7 @@ - name: No earned income period: 2022 input: - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false state_code: NM earned_income: 0 is_disabled: false @@ -29,7 +29,7 @@ - name: No earned income but disabled - eligible period: 2022 input: - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false state_code: NM earned_income: 0 is_disabled: true @@ -43,7 +43,7 @@ - name: Over income threshold period: 2022 input: - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false state_code: NM earned_income: 0 is_disabled: false @@ -57,7 +57,7 @@ - name: Receives tanf period: 2022 input: - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false state_code: NM earned_income: 0 is_disabled: false @@ -71,7 +71,7 @@ - name: Eligible period: 2022 input: - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false state_code: NM earned_income: 1 is_disabled: false @@ -101,7 +101,6 @@ tax_units: tax_unit: members: [head, spouse] - head_is_dependent_elsewhere: false filing_status: JOINT nm_modified_gross_income: 30_159 households: @@ -130,7 +129,6 @@ tax_units: tax_unit: members: [head, spouse] - head_is_dependent_elsewhere: false filing_status: JOINT nm_modified_gross_income: 30_159 households: @@ -159,7 +157,6 @@ tax_units: tax_unit: members: [head, spouse] - head_is_dependent_elsewhere: false filing_status: JOINT nm_modified_gross_income: 30_159 households: @@ -188,7 +185,6 @@ tax_units: tax_unit: members: [head, spouse] - head_is_dependent_elsewhere: false filing_status: JOINT nm_modified_gross_income: 30_159 households: @@ -217,7 +213,6 @@ tax_units: tax_unit: members: [head, spouse] - head_is_dependent_elsewhere: false filing_status: JOINT nm_modified_gross_income: 30_159 households: @@ -230,7 +225,7 @@ - name: Reported TANF receipt disqualifies despite zero computed TANF period: 2022 input: - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false state_code: NM earned_income: 0 is_disabled: true @@ -241,3 +236,89 @@ nm_modified_gross_income: 30_159 output: nm_cdcc_eligible: false + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the head) + period: 2022 + input: + people: + person_a: + age: 30 + earned_income: 1 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + earned_income: 1 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + nm_modified_gross_income: 30_159 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_cdcc_eligible: true + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the spouse) + period: 2022 + input: + people: + person_b: + age: 30 + earned_income: 1 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + earned_income: 1 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + nm_modified_gross_income: 30_159 + households: + household: + members: [person_b, person_a] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_cdcc_eligible: true + +- name: Joint return where both spouses are a dependent of another taxpayer + period: 2022 + input: + people: + person_a: + age: 30 + earned_income: 1 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + earned_income: 1 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + nm_modified_gross_income: 30_159 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + nm_cdcc_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml index df55277d73a..518854ff24e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml @@ -4,7 +4,7 @@ state_code: NM age: 64 other_medical_expenses: 30_000 - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_medical_expense_credit: 0 @@ -16,7 +16,7 @@ medicare_enrolled: false other_medical_expenses: 27_999 filing_status: JOINT - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_medical_expense_credit: 0 @@ -27,7 +27,7 @@ age: 65 other_medical_expenses: 28_000 filing_status: JOINT - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_medical_expense_credit: 2_800 @@ -38,7 +38,7 @@ age: 65 other_medical_expenses: 28_000 filing_status: SEPARATE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_medical_expense_credit: 1_400 @@ -49,7 +49,7 @@ age: 65 other_medical_expenses: 40_000 filing_status: SINGLE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_medical_expense_credit: 2_800 @@ -60,6 +60,86 @@ age: 65 other_medical_expenses: 40_000 filing_status: SINGLE - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true + output: + nm_medical_expense_credit: 0 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the head) + period: 2023 + input: + people: + person_a: + age: 66 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 66 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + itemized_medical_expenses: 28_000 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_medical_expense_credit: 2_800 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the spouse) + period: 2023 + input: + people: + person_b: + age: 66 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 66 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + itemized_medical_expenses: 28_000 + households: + household: + members: [person_b, person_a] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_medical_expense_credit: 2_800 + +- name: Joint return where both spouses are a dependent of another taxpayer + period: 2023 + input: + people: + person_a: + age: 66 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 66 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + itemized_medical_expenses: 28_000 + households: + household: + members: [person_a, person_b] + state_code: NM output: nm_medical_expense_credit: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.yaml index 6f65f043918..bcb29cf3062 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.yaml @@ -3,7 +3,7 @@ input: exemptions: 0 state_code: NM - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_deduction_for_certain_dependents_eligible: true @@ -12,7 +12,7 @@ input: exemptions: 1 state_code: NM - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_deduction_for_certain_dependents_eligible: false @@ -22,6 +22,86 @@ input: exemptions: 0 state_code: NM - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true + output: + nm_deduction_for_certain_dependents_eligible: false + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + exemptions: 0 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_deduction_for_certain_dependents_eligible: true + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + exemptions: 0 + households: + household: + members: [person_b, person_a] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_deduction_for_certain_dependents_eligible: true + +- name: Joint return where both spouses are a dependent of another taxpayer + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + exemptions: 0 + households: + household: + members: [person_a, person_b] + state_code: NM output: nm_deduction_for_certain_dependents_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_2021_income_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_2021_income_rebate.yaml index 72c2bb91a67..3a1c22772bc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_2021_income_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_2021_income_rebate.yaml @@ -4,7 +4,7 @@ state_code: NM adjusted_gross_income: 74_999 filing_status: SINGLE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_2021_income_rebate: 250 @@ -14,7 +14,7 @@ state_code: NM adjusted_gross_income: 75_000 filing_status: SEPARATE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_2021_income_rebate: 0 @@ -24,7 +24,7 @@ state_code: NM adjusted_gross_income: 149_999 filing_status: JOINT - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_2021_income_rebate: 500 @@ -34,7 +34,7 @@ state_code: NM adjusted_gross_income: 150_000 filing_status: SURVIVING_SPOUSE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_2021_income_rebate: 0 @@ -44,6 +44,86 @@ state_code: NM adjusted_gross_income: 149_999 filing_status: JOINT - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true + output: + nm_2021_income_rebate: 0 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + adjusted_gross_income: 50_000 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_2021_income_rebate: 500 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + adjusted_gross_income: 50_000 + households: + household: + members: [person_b, person_a] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_2021_income_rebate: 500 + +- name: Joint return where both spouses are a dependent of another taxpayer + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + adjusted_gross_income: 50_000 + households: + household: + members: [person_a, person_b] + state_code: NM output: nm_2021_income_rebate: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_additional_2021_income_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_additional_2021_income_rebate.yaml index ddf2f6150d1..91a64599414 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_additional_2021_income_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_additional_2021_income_rebate.yaml @@ -3,7 +3,7 @@ input: state_code: NM filing_status: JOINT - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_additional_2021_income_rebate: 1_000 @@ -12,7 +12,7 @@ input: state_code: NM filing_status: SINGLE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_additional_2021_income_rebate: 500 @@ -21,7 +21,7 @@ input: state_code: NM filing_status: SEPARATE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_additional_2021_income_rebate: 500 @@ -30,7 +30,7 @@ input: state_code: NM filing_status: SURVIVING_SPOUSE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_additional_2021_income_rebate: 1_000 @@ -39,7 +39,7 @@ input: state_code: NM filing_status: HEAD_OF_HOUSEHOLD - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_additional_2021_income_rebate: 1_000 @@ -48,6 +48,83 @@ input: state_code: NM filing_status: SEPARATE - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true + output: + nm_additional_2021_income_rebate: 0 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_additional_2021_income_rebate: 1_000 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_additional_2021_income_rebate: 1_000 + +- name: Joint return where both spouses are a dependent of another taxpayer + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: NM output: nm_additional_2021_income_rebate: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_supplemental_2021_income_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_supplemental_2021_income_rebate.yaml index ac64cc9f434..0b1b8565f7a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_supplemental_2021_income_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/2021_income/nm_supplemental_2021_income_rebate.yaml @@ -3,7 +3,7 @@ input: state_code: NM filing_status: JOINT - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_supplemental_2021_income_rebate: 1_000 @@ -12,7 +12,7 @@ input: state_code: NM filing_status: SINGLE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_supplemental_2021_income_rebate: 500 @@ -21,7 +21,7 @@ input: state_code: NM filing_status: SEPARATE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_supplemental_2021_income_rebate: 500 @@ -30,7 +30,7 @@ input: state_code: NM filing_status: SURVIVING_SPOUSE - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_supplemental_2021_income_rebate: 1_000 @@ -39,7 +39,7 @@ input: state_code: NM filing_status: HEAD_OF_HOUSEHOLD - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false output: nm_supplemental_2021_income_rebate: 1_000 @@ -48,6 +48,83 @@ input: state_code: NM filing_status: HEAD_OF_HOUSEHOLD - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true + output: + nm_supplemental_2021_income_rebate: 0 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_supplemental_2021_income_rebate: 1_000 + +- name: Joint return where only one spouse is a dependent of another taxpayer (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: NM + output: + # A spouse who is not a dependent may still qualify (PIT-RC instructions). + nm_supplemental_2021_income_rebate: 1_000 + +- name: Joint return where both spouses are a dependent of another taxpayer + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: NM output: nm_supplemental_2021_income_rebate: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.yaml new file mode 100644 index 00000000000..cd98af8b0dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.yaml @@ -0,0 +1,94 @@ +- name: 2025 single filer who can be claimed as a dependent + period: 2025 + input: + state_code: NY + filing_status: SINGLE + claimed_as_dependent_on_another_return: true + output: + ny_standard_deduction: 3_100 + +- name: 2025 single filer who cannot be claimed as a dependent + period: 2025 + input: + state_code: NY + filing_status: SINGLE + claimed_as_dependent_on_another_return: false + output: + ny_standard_deduction: 8_000 + +- name: 2025 head of household who can be claimed as a dependent keeps the regular amount + period: 2025 + input: + state_code: NY + filing_status: HEAD_OF_HOUSEHOLD + claimed_as_dependent_on_another_return: true + output: + # The IT-201 table gives the dependent amount only to filing status 1. + ny_standard_deduction: 11_200 + +- name: 2025 separate filer who can be claimed as a dependent keeps the regular amount + period: 2025 + input: + state_code: NY + filing_status: SEPARATE + claimed_as_dependent_on_another_return: true + output: + ny_standard_deduction: 8_000 + +- name: 2025 joint return where only one spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: NY + output: + # The IT-201 table gives married filing jointly $16,050 whether or not + # item C is marked. + ny_standard_deduction: 16_050 + +- name: 2025 joint return where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + employment_income: 8_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: NY + output: + # The IT-201 table gives married filing jointly $16,050 whether or not + # item C is marked. + ny_standard_deduction: 16_050 diff --git a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml index 6c2182a4783..7818ce120d2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml @@ -68,7 +68,7 @@ state_code: OR filing_status: JOINT age_head: 35 - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true tax_unit_earned_income: 600 output: # $1100 base @@ -80,7 +80,7 @@ state_code: OR filing_status: SINGLE age_head: 35 - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true tax_unit_earned_income: 1_000 output: # $1000 income + $350 addition, below the $2,350 normal standard deduction cap. @@ -92,7 +92,7 @@ state_code: OR filing_status: SINGLE age: 35 - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true tax_unit_earned_income: 1_000 blind_head: true output: @@ -137,3 +137,57 @@ output: # $2,835 base + $1,200 aged or_standard_deduction: 4_035 + +- name: 2025 joint return limited to the dependent amount when either spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 35 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 35 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + tax_unit_earned_income: 3_000 + households: + household: + members: [person_a, person_b] + state_code: OR + output: + # max($1,350, $3,000 + $450) = $3,450, below the $5,670 joint amount. + or_standard_deduction: 3_450 + +- name: 2025 joint return limited to the dependent amount when either spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 35 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 35 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + tax_unit_earned_income: 3_000 + households: + household: + members: [person_b, person_a] + state_code: OR + output: + # max($1,350, $3,000 + $450) = $3,450, below the $5,670 joint amount. + or_standard_deduction: 3_450 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.yaml index 5584f3c0288..b5cf4a21af1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.yaml @@ -2,7 +2,7 @@ period: 2021 input: state_code: VA - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true va_aged_blind_exemption: 0 va_age_deduction: 0 va_military_benefit_subtraction: 0 @@ -14,7 +14,7 @@ period: 2021 input: state_code: VA - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false va_aged_blind_exemption: 1_600 va_age_deduction: 12_000 va_military_benefit_subtraction: 0 @@ -26,10 +26,60 @@ period: 2021 input: state_code: VA - head_is_dependent_elsewhere: false + claimed_as_dependent_on_another_return: false va_aged_blind_exemption: 0 va_age_deduction: 0 va_military_benefit_subtraction: 0 va_federal_state_employees_subtraction: 0 output: va_low_income_tax_credit_eligible: true + +- name: Joint return is ineligible when either spouse is claimed as a dependent (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: VA + output: + va_low_income_tax_credit_eligible: false + +- name: Joint return is ineligible when either spouse is claimed as a dependent (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: VA + output: + va_low_income_tax_credit_eligible: false diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.yaml new file mode 100644 index 00000000000..80029885451 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.yaml @@ -0,0 +1,114 @@ +- name: Head is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [head, spouse] + output: + head_or_spouse_is_dependent_elsewhere: true + +- name: Spouse is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + output: + head_or_spouse_is_dependent_elsewhere: true + +- name: Both spouses are claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + output: + head_or_spouse_is_dependent_elsewhere: true + +- name: Neither spouse is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [head, spouse] + output: + head_or_spouse_is_dependent_elsewhere: false + +- name: A dependent claimed elsewhere is not a filer + period: 2025 + input: + people: + head: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + child: + age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, child] + output: + head_or_spouse_is_dependent_elsewhere: false + +- name: Single filer claimed elsewhere + period: 2025 + input: + people: + head: + age: 19 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head] + output: + head_or_spouse_is_dependent_elsewhere: true diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.yaml new file mode 100644 index 00000000000..ac5381a1b4b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.yaml @@ -0,0 +1,114 @@ +- name: Head is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [head, spouse] + output: + head_spouse_count_not_dependent_elsewhere: 1 + +- name: Spouse is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + output: + head_spouse_count_not_dependent_elsewhere: 1 + +- name: Both spouses are claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + output: + head_spouse_count_not_dependent_elsewhere: 0 + +- name: Neither spouse is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [head, spouse] + output: + head_spouse_count_not_dependent_elsewhere: 2 + +- name: A dependent claimed elsewhere is not a filer + period: 2025 + input: + people: + head: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + child: + age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, child] + output: + head_spouse_count_not_dependent_elsewhere: 1 + +- name: Single filer claimed elsewhere + period: 2025 + input: + people: + head: + age: 19 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head] + output: + head_spouse_count_not_dependent_elsewhere: 0 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py index e3d3745bdc5..e8575cdcc7e 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py @@ -7,13 +7,23 @@ class basic_standard_deduction(Variable): label = "Basic standard deduction" definition_period = YEAR unit = USD - reference = "https://www.law.cornell.edu/uscode/text/26/63#c_2" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/63#c_2", + "https://www.law.cornell.edu/uscode/text/26/63#c_5", + # Standard Deduction Worksheet for Dependents: "Use this worksheet + # only if someone can claim you, or your spouse if filing jointly, + # as a dependent." + "https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf#page=35", + ) def formula(tax_unit, period, parameters): p = parameters(period).gov.irs.deductions.standard filing_status = tax_unit("filing_status", period) separate_filer_itemizes = tax_unit("separate_filer_itemizes", period) - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # A joint return uses the dependent worksheet, with the couple's + # earned income and the joint amount as the cap, when either spouse + # can be claimed. + dependent_elsewhere = tax_unit("head_or_spouse_is_dependent_elsewhere", period) standard_deduction = p.amount[filing_status] standard_deduction_if_dependent = min_( standard_deduction, diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py index c4f098d02f1..a98403539f7 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py @@ -8,17 +8,20 @@ class hi_food_excise_credit(Variable): defined_for = StateCode.HI unit = USD definition_period = YEAR - reference = "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=44" + reference = ( + "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=50", + "https://files.hawaii.gov/tax/forms/2025/n311_i.pdf#page=1", + ) def formula(tax_unit, period, parameters): - # filer can not be a dependent on another return - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) - total_amount = add( - tax_unit, - period, - [ - "hi_food_excise_credit_minor_child_amount", - "hi_food_excise_exemption_amount", - ], + # HRS 235-55.85(a): only a taxpayer who cannot be claimed as a + # dependent may claim the credit, for each qualified exemption. The + # exemption amount counts only filers who cannot be claimed when + # either can; such a return claims no dependents (IRS Publication + # 501), so it gets no minor child amount. + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + exemption_amount = tax_unit("hi_food_excise_exemption_amount", period) + minor_child_amount = tax_unit( + "hi_food_excise_credit_minor_child_amount", period ) - return ~dependent_elsewhere * total_amount + return exemption_amount + where(dependent_filer, 0, minor_child_amount) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py index 1b4e22e61bc..d11bb0a6cac 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py @@ -7,6 +7,11 @@ class hi_food_excise_exemption_amount(Variable): label = "Exemption amount for Hawaii Food/Excise Tax Credit" definition_period = YEAR defined_for = StateCode.HI + reference = ( + "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=50", + "https://files.hawaii.gov/tax/forms/2025/n311_i.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", + ) def formula(tax_unit, period, parameters): p = parameters(period).gov.states.hi.tax.income.credits.food_excise_tax @@ -45,4 +50,15 @@ def formula(tax_unit, period, parameters): claimable_exemptions = exemptions - minor_children else: claimable_exemptions = exemptions - return claimable_exemptions * amount_per_exemption + # Form N-311 counts only people who cannot be claimed as a dependent + # by another taxpayer. A return on which a filer can be claimed claims + # no dependents (IRS Publication 501), so it counts only the filers + # who cannot be claimed. + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + independent_filers = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period + ) + qualified_exemptions = where( + dependent_filer, independent_filers, claimable_exemptions + ) + return qualified_exemptions * amount_per_exemption diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py index 8fd79179391..3a229d50ebb 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py @@ -12,6 +12,7 @@ class hi_act_115_rebate(Variable): "https://tax.hawaii.gov/act-115-ref/", "https://data.capitol.hawaii.gov/sessions/session2022/bills/SB514_CD2_.HTM", "https://files.hawaii.gov/tax/news/announce/ann22-03.pdf#page=2", + "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=45", ) def formula(tax_unit, period, parameters): @@ -39,6 +40,15 @@ def formula(tax_unit, period, parameters): # person in the tax unit is one exemption. exemptions = tax_unit("exemptions_count", period) # A person who can be claimed as a dependent by another taxpayer is - # not a qualifying resident taxpayer. - head_is_dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) - return where(head_is_dependent_elsewhere, 0, amount_per_exemption * exemptions) + # not a qualifying resident taxpayer, and each qualifying resident + # taxpayer claims the refund for the exemptions they are entitled to. + # A filer who can be claimed has no exemption (HRS 235-54(a)), and a + # return on which a filer can be claimed claims no dependents (IRS + # Publication 501), so such a return counts only the filers who + # cannot be claimed. + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + independent_filers = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period + ) + qualified_exemptions = where(dependent_filer, independent_filers, exemptions) + return amount_per_exemption * qualified_exemptions diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py index e0a69bef748..81a87e2876e 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py @@ -8,18 +8,36 @@ class hi_tax_credit_for_low_income_household_renters(Variable): unit = USD definition_period = YEAR defined_for = "hi_tax_credit_for_low_income_household_renters_eligible" + reference = ( + "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=48", + "https://files.hawaii.gov/tax/forms/2025/schx_i.pdf#page=1", + ) def formula(tax_unit, period, parameters): p = parameters(period).gov.states.hi.tax.income.credits.lihrtc - # Aged extra exemptions - aged_head = (tax_unit("age_head", period) >= p.aged_age_threshold).astype(int) - aged_spouse = (tax_unit("age_spouse", period) >= p.aged_age_threshold).astype( - int + # Schedule X lines 8-10 count only people who cannot be claimed as a + # dependent by another taxpayer, with an extra exemption for each + # such filer aged 65 or over. A return on which a filer can be + # claimed claims no dependents (IRS Publication 501), so it counts + # only the filers who cannot be claimed. + aged_head = (tax_unit("age_head", period) >= p.aged_age_threshold) & ~tax_unit( + "head_is_dependent_elsewhere", period ) + aged_spouse = ( + tax_unit("age_spouse", period) >= p.aged_age_threshold + ) & ~tax_unit("spouse_is_dependent_elsewhere", period) + aged_exemptions = aged_head.astype(int) + aged_spouse.astype(int) - aged_exemptions = aged_head + aged_spouse - exemptions = tax_unit("exemptions_count", period) + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + independent_filers = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period + ) + exemptions = where( + dependent_filer, + independent_filers, + tax_unit("exemptions_count", period), + ) total_exemptions = exemptions + aged_exemptions return p.amount * total_exemptions diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py index 3c99b935916..db97f6a626f 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py @@ -6,16 +6,26 @@ class hi_tax_credit_for_low_income_household_renters_eligible(Variable): entity = TaxUnit label = "Eligible for the Hawaii low income household renters tax credit" definition_period = YEAR - reference = "https://files.hawaii.gov/tax/legal/har/har_235.pdf#page=105" # §18-235-55.7 (b) + reference = ( + "https://files.hawaii.gov/tax/legal/har/har_235.pdf#page=105", # §18-235-55.7 (b) + "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=48", + "https://files.hawaii.gov/tax/forms/2025/schx_i.pdf#page=1", + ) defined_for = StateCode.HI def formula(tax_unit, period, parameters): - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # HRS 235-55.7(b) allows the credit to each resident taxpayer who + # cannot be claimed as a dependent. Schedule X stops at line 3 only + # for the first-listed filer; since a couple may list either spouse + # first, a joint return qualifies unless both spouses can be claimed. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) p = parameters(period).gov.states.hi.tax.income.credits.lihrtc agi = tax_unit("hi_agi", period) rent = add(tax_unit, period, ["rent"]) return ( (rent > p.eligibility.rent_threshold) & (agi < p.eligibility.agi_limit) - & ~dependent_elsewhere + & independent_filer ) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py index 5e84412ac6c..70a783508dd 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py @@ -10,6 +10,11 @@ class hi_deductions(Variable): "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=15\n" # Itemized Deduction "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=20" # Standard Deduction ) + reference = ( + "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=11", + # PDF pages 15, 20 + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=15", + ) definition_period = YEAR defined_for = StateCode.HI @@ -25,14 +30,26 @@ def formula(tax_unit, period, parameters): filing_status_eligible = ( total_itemized_deduction > p.threshold.deductions[filing_status] ) - is_dependent_on_another_return = tax_unit("head_is_dependent_elsewhere", period) - standard_cap = min_(tax_unit_earned_income, standard_deduction) - dependent_floor = max_(p.threshold.dependent, standard_cap) + # HRS 235-2.4(a)(3) applies IRC 63(c)(5): the standard deduction of a + # filer who can be claimed as a dependent is the greater of $500 or + # earned income, up to the regular amount. As federally, a joint + # return is limited when either spouse can be claimed. + is_dependent_on_another_return = tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) + dependent_standard_deduction = min_( + max_(p.threshold.dependent, tax_unit_earned_income), standard_deduction + ) + applicable_standard_deduction = where( + is_dependent_on_another_return, + dependent_standard_deduction, + standard_deduction, + ) dependent_eligible = is_dependent_on_another_return & ( - total_itemized_deduction > dependent_floor + total_itemized_deduction > dependent_standard_deduction ) itemized_deductions_eligible = filing_status_eligible | dependent_eligible itemized_deduction = where( itemized_deductions_eligible, total_itemized_deduction, 0 ) - return max_(itemized_deduction, standard_deduction) + return max_(itemized_deduction, applicable_standard_deduction) diff --git a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py index c0ea36df12a..d761c2d8ecf 100644 --- a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py +++ b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py @@ -6,11 +6,21 @@ class me_sales_tax_fairness_credit_eligible(Variable): entity = TaxUnit label = "Eligible for the Maine sales tax fairness credit" definition_period = YEAR - reference = "https://legislature.maine.gov/statutes/36/title36sec5213-A.html" + reference = ( + "https://legislature.maine.gov/statutes/36/title36sec5213-A.html", + "https://www.maine.gov/revenue/sites/maine.gov.revenue/files/inline-files/25_1040me_sch_ptfc_fillable.pdf#page=3", + ) defined_for = StateCode.ME def formula(tax_unit, period, parameters): - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # 36 MRSA 5213-A(6)(C) excludes individuals who may be claimed as a + # dependent on another return, and subsection 2 allows each resident + # individual the base credit for the return's filing status. On a + # joint return where only one spouse can be claimed, the other spouse + # still qualifies for the joint credit. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) filing_status = tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE - return ~dependent_elsewhere & ~separate + return independent_filer & ~separate diff --git a/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py b/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py index 5974fd46303..1c77fb504ce 100644 --- a/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py +++ b/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py @@ -11,6 +11,8 @@ class mi_exemptions(Variable): reference = ( "https://www.legislature.mi.gov/Laws/MCL?objectName=mcl-206-30", "https://www.legislature.mi.gov/Publications/TaxpayerGuide.pdf", + "https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2025/MI-1040-Book.pdf#page=10", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", ) def formula(tax_unit, period, parameters): @@ -31,16 +33,31 @@ def formula(tax_unit, period, parameters): ) disabled_veteran_exemption = disabled_veterans * p.disabled.amount.veteran - # Dependent-on-other-return exemptions - filing_status = tax_unit("filing_status", period) - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) - dependent_exemption = ( - dependent_elsewhere * p.dependent_on_other_return[filing_status] + # MCL 206.30(4): an individual who can be claimed as a dependent on + # another return gets no personal exemption but subtracts $1,500. + # This applies to each spouse separately, so on a joint return where + # only one spouse can be claimed, the other keeps their own personal + # exemption (and any stillbirth exemption). A joint return on which + # either spouse can be claimed claims no dependents (IRS Publication + # 501, "Dependent Taxpayer Test"). + filers = add(tax_unit, period, ["is_tax_unit_head_or_spouse"]) + independent_filers = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period ) + dependent_filers = filers - independent_filers + stillborn = tax_unit("tax_unit_stillborn_children", period) + independent_filer_exemptions = ( + independent_filers + where(independent_filers > 0, stillborn, 0) + ) * p.personal + dependent_filer_exemptions = dependent_filers * p.dependent_on_other_return # Total exemptions return ( - where(dependent_elsewhere, dependent_exemption, personal_exemption) + where( + dependent_filers > 0, + independent_filer_exemptions + dependent_filer_exemptions, + personal_exemption, + ) + disabled_exemption + disabled_veteran_exemption ) diff --git a/policyengine_us/variables/gov/states/mo/tax/income/credits/mo_wftc_eligible.py b/policyengine_us/variables/gov/states/mo/tax/income/credits/mo_wftc_eligible.py index d1b71e8d93e..582fdb3ed2a 100644 --- a/policyengine_us/variables/gov/states/mo/tax/income/credits/mo_wftc_eligible.py +++ b/policyengine_us/variables/gov/states/mo/tax/income/credits/mo_wftc_eligible.py @@ -8,7 +8,8 @@ class mo_wftc_eligible(Variable): definition_period = YEAR reference = ( "https://revisor.mo.gov/main/OneSection.aspx?section=143.177&bid=49978&hl=", - "https://dor.mo.gov/forms/MO-1040%20Instructions_2025.pdf#page=43", + # PDF pages 6, 43 + "https://dor.mo.gov/forms/MO-1040%20Instructions_2025.pdf#page=6", ) defined_for = StateCode.MO @@ -23,10 +24,17 @@ def formula(tax_unit, period, parameters): # married filing separately. filing_status = tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE - # The 2024 and 2025 Form MO-WFTC checklists also stop filers - # claimed as a dependent on another return; the 2023 form does not. - excluded_dependent = p.dependent_filers_excluded & tax_unit( - "head_is_dependent_elsewhere", period + # The 2024 and 2025 Form MO-WFTC checklists also stop filers with a + # filing status of "claimed as a dependent"; the 2023 form does not. + # A couple filing a joint federal return files Form MO-1040 as + # married filing combined even when a spouse can be claimed (2025 + # MO-1040 instructions, "Filing status"), so the stop never applies + # to a joint return. + joint = filing_status == filing_status.possible_values.JOINT + excluded_dependent = ( + p.dependent_filers_excluded + & ~joint + & tax_unit("head_or_spouse_is_dependent_elsewhere", period) ) # Form MO-WFTC applies Missouri's own investment income limit, # reflecting IRC Section 32(i) as of January 1, 2021 per RSMo diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py index ea1c0e9fd46..48198e1b904 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py @@ -6,13 +6,22 @@ class nm_cdcc_eligible(Variable): entity = TaxUnit label = "Eligible household for the New Mexico dependent child day care credit" definition_period = YEAR - reference = "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503752/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsfAEwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA" + reference = ( + "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503752/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsfAEwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA", + "https://realfile.tax.newmexico.gov/2025pit-rc-ins.pdf#page=2", + ) defined_for = StateCode.NM def formula(tax_unit, period, parameters): person = tax_unit.members - # Filer can not be a dependent on another tax return - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # Filer can not be a dependent on another tax return, but "If you are + # a dependent with a spouse who was not a dependent of another + # taxpayer, your spouse may still qualify to claim rebates or + # credits" (PIT-RC instructions). So only a return on which every + # filer is a dependent elsewhere is barred. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) p = parameters(period).gov.states.nm.tax.income.credits.cdcc # Filer has to be be gainfully employed to receive credit has_earnings = person("earned_income", period) > 0 @@ -43,8 +52,5 @@ def formula(tax_unit, period, parameters): ) income_eligible = nm_modified_gross_income <= income_limit return ( - ~dependent_elsewhere - & employment_eligible - & ~receives_tanf - & income_eligible + independent_filer & employment_eligible & ~receives_tanf & income_eligible ) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py index e516884f45e..205f3b1b0e4 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py @@ -6,7 +6,10 @@ class nm_medical_expense_credit(Variable): entity = TaxUnit label = "New Mexico unreimbursed medical expense care credit" definition_period = YEAR - reference = "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503776/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDswgGwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA" + reference = ( + "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503776/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDswgGwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA", + "https://realfile.tax.newmexico.gov/2025pit-rc-ins.pdf#page=2", + ) defined_for = StateCode.NM def formula(tax_unit, period, parameters): @@ -17,8 +20,15 @@ def formula(tax_unit, period, parameters): medical_expense = tax_unit("itemized_medical_expenses", period) age_eligible = tax_unit.any(age >= p.age_eligibility) expense_eligible = medical_expense >= p.min_expenses - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) - eligible = age_eligible & expense_eligible & ~dependent_elsewhere + # The rebate and credit schedule bars a filer who is a dependent of + # another taxpayer, but "If you are a dependent with a spouse who was + # not a dependent of another taxpayer, your spouse may still qualify + # to claim rebates or credits" (PIT-RC instructions). So only a return + # on which every filer is a dependent elsewhere is barred. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) + eligible = age_eligible & expense_eligible & independent_filer # exemption is halved for married filing separately filing_status = tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE diff --git a/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py b/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py index d0415b60595..d66ef23f1a6 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py @@ -6,12 +6,22 @@ class nm_deduction_for_certain_dependents_eligible(Variable): entity = TaxUnit label = "Eligibility for New Mexico deduction for certain dependents" definition_period = YEAR - reference = "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503892/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgAcATgBMASgA0ybKUIQAiokK4AntADkW6REJhcCFWs069BoyADKeUgCFNAJQCiAGRcA1AIIA5AMIu0qRgAEbQpOySkkA" + reference = ( + "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503892/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgAcATgBMASgA0ybKUIQAiokK4AntADkW6REJhcCFWs069BoyADKeUgCFNAJQCiAGRcA1AIIA5AMIu0qRgAEbQpOySkkA", + "https://realfile.tax.newmexico.gov/2025pit-rc-ins.pdf#page=2", + ) defined_for = StateCode.NM def formula(tax_unit, period, parameters): - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # The deduction uses the same "not a dependent of another individual" + # test as the rebates and credits on Schedule PIT-RC, whose + # instructions let a spouse who is not a dependent still qualify. So + # only a return on which every filer is a dependent elsewhere is + # barred. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) # deduction does not apply if an exemption under IRS 151 is claimed; # IRC 151 refers to the federal personal exemption federal_exemption_amount = tax_unit("exemptions", period) - return ~dependent_elsewhere & (federal_exemption_amount == 0) + return independent_filer & (federal_exemption_amount == 0) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py index 1f37e1c7c21..8ce48277203 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py @@ -6,15 +6,26 @@ class nm_2021_income_rebate(Variable): entity = TaxUnit label = "New Mexico 2021 income tax rebate" definition_period = YEAR - reference = "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503708/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsPABwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA" + reference = ( + "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503708/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsPABwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA", + # PDF pages 2-3 + "https://realfile.tax.newmexico.gov/2021pit-rc-ins.pdf#page=2", + ) defined_for = StateCode.NM def formula(tax_unit, period, parameters): - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # The rebate and credit schedule bars a filer who is a dependent of + # another taxpayer, but "If you are a dependent with a spouse who was + # not a dependent of another taxpayer, your spouse may still qualify + # to claim rebates or credits" (PIT-RC instructions). So only a return + # on which every filer is a dependent elsewhere is barred. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) agi = tax_unit("adjusted_gross_income", period) filing_status = tax_unit("filing_status", period) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] income_eligible = agi < p.main.income_limit[filing_status] - eligible = income_eligible & ~dependent_elsewhere + eligible = income_eligible & independent_filer amount_if_eligible = p.main.amount[filing_status] return eligible * amount_if_eligible diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py index 891ce07d54e..dd68e862de4 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py @@ -6,11 +6,22 @@ class nm_additional_2021_income_rebate(Variable): entity = TaxUnit label = "New Mexico additional 2021 income tax rebate" definition_period = YEAR - reference = "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503710/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsHHgEoANMmylCEAIqJCuAJ7QA5BskRCYXAiUr1WnXoMgAynlIAhdQCUAogBknANQCCAOQDCTyVIwACNoUnZxcSA" + reference = ( + "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503710/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsHHgEoANMmylCEAIqJCuAJ7QA5BskRCYXAiUr1WnXoMgAynlIAhdQCUAogBknANQCCAOQDCTyVIwACNoUnZxcSA", + # PDF pages 2-3 + "https://realfile.tax.newmexico.gov/2021pit-rc-ins.pdf#page=2", + ) defined_for = StateCode.NM def formula(tax_unit, period, parameters): - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # The rebate and credit schedule bars a filer who is a dependent of + # another taxpayer, but "If you are a dependent with a spouse who was + # not a dependent of another taxpayer, your spouse may still qualify + # to claim rebates or credits" (PIT-RC instructions). So only a return + # on which every filer is a dependent elsewhere is barred. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] filing_status = tax_unit("filing_status", period) - return ~dependent_elsewhere * p.additional.amount[filing_status] + return independent_filer * p.additional.amount[filing_status] diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py index 7839b75873d..e98e3be36dd 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py @@ -6,11 +6,22 @@ class nm_supplemental_2021_income_rebate(Variable): entity = TaxUnit label = "New Mexico supplemental 2021 income tax rebate" definition_period = YEAR - reference = "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503706/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsPAGwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA" + reference = ( + "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503706/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgDsPAGwBKADTJspQhACKiQrgCe0AOSapEQmFwJlqjdt37DIAMp5SAIQ0AlAKIAZZwDUAggDkAws5SpGAARtCk7BISQA", + # PDF pages 2-3 + "https://realfile.tax.newmexico.gov/2021pit-rc-ins.pdf#page=2", + ) defined_for = StateCode.NM def formula(tax_unit, period, parameters): - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # The rebate and credit schedule bars a filer who is a dependent of + # another taxpayer, but "If you are a dependent with a spouse who was + # not a dependent of another taxpayer, your spouse may still qualify + # to claim rebates or credits" (PIT-RC instructions). So only a return + # on which every filer is a dependent elsewhere is barred. + independent_filer = ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + ) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] filing_status = tax_unit("filing_status", period) - return ~dependent_elsewhere * p.supplemental.amount[filing_status] + return independent_filer * p.supplemental.amount[filing_status] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.py index d98250ebddb..1720bb49cd2 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/ny_standard_deduction.py @@ -7,17 +7,21 @@ class ny_standard_deduction(Variable): label = "NY standard deduction" unit = USD definition_period = YEAR - reference = "https://www.nysenate.gov/legislation/laws/TAX/613" + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/614", + # Standard deduction table: the dependent amount applies only to + # filing status 1, "Single and you marked item C Yes". + "https://www.tax.ny.gov/pdf/2025/inc/it201i_2025.pdf#page=11", + ) defined_for = StateCode.NY def formula(tax_unit, period, parameters): - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) - standard_deduction = parameters( - period - ).gov.states.ny.tax.income.deductions.standard + p = parameters(period).gov.states.ny.tax.income.deductions.standard filing_status = tax_unit("filing_status", period) + single = filing_status == filing_status.possible_values.SINGLE + dependent_elsewhere = tax_unit("head_or_spouse_is_dependent_elsewhere", period) return where( - dependent_elsewhere, - standard_deduction.dependent_elsewhere, - standard_deduction.amount[filing_status], + single & dependent_elsewhere, + p.dependent_elsewhere, + p.amount[filing_status], ) diff --git a/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py b/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py index 6d8dde3554d..eb3c543e6fa 100644 --- a/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py +++ b/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py @@ -8,9 +8,10 @@ class or_standard_deduction(Variable): unit = USD definition_period = YEAR documentation = "Oregon standard deduction, including bonus for aged or blind and special rules for filers who are claimable as dependents." - reference = ( # TODO: update + reference = ( "https://www.oregon.gov/dor/forms/FormsPubs/form-or-40-inst_101-040-1_2021.pdf#page=18", - "https://www.oregonlegislature.gov/bills_laws/ors/ors316.html", # Subsection 316.695 (7) + "https://www.oregonlegislature.gov/bills_laws/ors/ors316.html", # ORS 316.695(7)-(8) + "https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2025.pdf#page=96", ) defined_for = StateCode.OR @@ -25,7 +26,11 @@ def formula(tax_unit, period, parameters): claimable_dep_earned_amount = ( earned_income + p.claimable_as_dependent.earned_income_addition ) - dependent_elsewhere = tax_unit("head_is_dependent_elsewhere", period) + # ORS 316.695(8) limits the deduction as IRC 63(c)(5) does, so a joint + # return uses the couple's earned income and the joint amount when + # either spouse can be claimed (Publication OR-17, "Standard deduction + # worksheet for married filing jointly (MFJ) dependents"). + dependent_elsewhere = tax_unit("head_or_spouse_is_dependent_elsewhere", period) # Set floor and ceiling around earned income plus additional amount. floored_claimable_dep_amount = max_( claimable_dep_earned_amount, claimable_dep_floor diff --git a/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py b/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py index 82b96fe10d0..7b746711ad1 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py +++ b/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py @@ -7,16 +7,23 @@ class va_low_income_tax_credit_eligible(Variable): label = "Eligible for the Virginia Low Income Tax Credit" definition_period = YEAR defined_for = StateCode.VA + reference = ( + "https://law.lis.virginia.gov/vacode/title58.1/chapter3/section58.1-339.8/", + "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2025-760-instructions.pdf#page=30", + ) def formula(tax_unit, period, parameters): # Criteria 1: Not qualified to claim EITC if anyone in the tax unit claimed any of the following: p = parameters(period).gov.states.va.tax.income.credits.eitc.low_income_tax program_eligible = add(tax_unit, period, p.ineligible_programs) == 0 - # Criteria 2: Not qualified to claim EITC if the filier is claimed as a dependent on another taxpayer's return - - head_is_not_dependent_elsewhere = ~tax_unit( - "head_is_dependent_elsewhere", period + # Criteria 2: Not qualified if a filer is claimed as a dependent on + # another taxpayer's return. Va. Code 58.1-339.8 bars the credit + # against the tax of a dependent of another individual or couple; a + # joint return carries both spouses' tax, so either spouse being + # claimed bars it. + filer_is_dependent_elsewhere = tax_unit( + "head_or_spouse_is_dependent_elsewhere", period ) - return program_eligible & head_is_not_dependent_elsewhere + return program_eligible & ~filer_is_dependent_elsewhere diff --git a/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py new file mode 100644 index 00000000000..493bf92c3c4 --- /dev/null +++ b/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class head_or_spouse_is_dependent_elsewhere(Variable): + value_type = bool + entity = TaxUnit + definition_period = YEAR + label = "Head or spouse is a dependent elsewhere" + documentation = ( + "Whether the head or the spouse of this tax unit is claimed as a " + "dependent in another tax unit. Unlike head_is_dependent_elsewhere, " + "this does not depend on which spouse is labelled head." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + claimed_on_another_return = person( + "claimed_as_dependent_on_another_return", period + ) + filer = person("is_tax_unit_head_or_spouse", period) + return tax_unit.any(claimed_on_another_return & filer) diff --git a/policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py new file mode 100644 index 00000000000..348b7a993a2 --- /dev/null +++ b/policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class head_spouse_count_not_dependent_elsewhere(Variable): + value_type = int + entity = TaxUnit + definition_period = YEAR + label = "Head and spouse count not dependent elsewhere" + documentation = ( + "Number of tax unit heads and spouses who are not claimed as a " + "dependent in another tax unit." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + claimed_on_another_return = person( + "claimed_as_dependent_on_another_return", period + ) + filer = person("is_tax_unit_head_or_spouse", period) + return tax_unit.sum(filer & ~claimed_on_another_return) From 3ecc675fe669409f59f1a756cf30103598e59598 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 08:35:58 -0400 Subject: [PATCH 02/32] Keep tax units without a filer eligible where only claimed filers are barred The New Mexico, Maine and Hawaii renters gates counted filers who are not claimed elsewhere, so a tax unit with neither a head nor a spouse (252 in the default dataset) looked like one whose every filer was claimed. A new helper, every_filer_is_dependent_elsewhere, requires a claimed filer and no unclaimed one. Co-Authored-By: Claude Opus 5.5 --- ...t_elsewhere_head_spouse_swap_invariance.py | 19 ++-- .../every_filer_is_dependent_elsewhere.yaml | 89 +++++++++++++++++++ ...r_low_income_household_renters_eligible.py | 6 +- .../me_sales_tax_fairness_credit_eligible.py | 6 +- .../income/credits/cdcc/nm_cdcc_eligible.py | 9 +- .../credits/nm_medical_expense_credit.py | 6 +- ...duction_for_certain_dependents_eligible.py | 6 +- .../2021_rebate/nm_2021_income_rebate.py | 6 +- .../nm_additional_2021_income_rebate.py | 6 +- .../nm_supplemental_2021_income_rebate.py | 6 +- .../every_filer_is_dependent_elsewhere.py | 18 ++++ 11 files changed, 138 insertions(+), 39 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.yaml create mode 100644 policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py diff --git a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py index 8c3e6fc2c48..721d38dff05 100644 --- a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py +++ b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py @@ -33,9 +33,10 @@ 2. Monotonicity: marking another filer as claimed elsewhere never raises any of the eighteen amounts or turns an eligibility on. 3. The helper identities: `head_or_spouse_is_dependent_elsewhere` equals - `head_is_dependent_elsewhere | spouse_is_dependent_elsewhere`, and - `head_spouse_count_not_dependent_elsewhere` plus the claimed filers equals - the number of filers. + `head_is_dependent_elsewhere | spouse_is_dependent_elsewhere`, + `every_filer_is_dependent_elsewhere` equals their conjunction for a + couple, and `head_spouse_count_not_dependent_elsewhere` plus the claimed + filers equals the number of filers. """ import numpy as np @@ -173,9 +174,11 @@ def _crafted_cases(states, review_case_only=False): "rent": 9_000.0, "other_medical_expenses": 30_000.0, } + # One claimed adult covers both labellings of a claimed spouse, since each + # case runs with the labels exchanged. cases = [] for state in states: - for claimed in CLAIM_PATTERNS[:3]: + for claimed in [(True, False), (True, True)]: cases += [ { "state": state, @@ -291,6 +294,8 @@ def _check_swap(units, year, outputs): np.testing.assert_array_equal(either, np.maximum(head, spouse)) independent = _calc(sim, "head_spouse_count_not_dependent_elsewhere", year) np.testing.assert_array_equal(independent + head + spouse, 2) + every = _calc(sim, "every_filer_is_dependent_elsewhere", year) + np.testing.assert_array_equal(every, np.minimum(head, spouse)) def _monotone_variants(unit): @@ -329,19 +334,19 @@ def _check_monotone(units, 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=6, + max_examples=4, deadline=None, derandomize=True, suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], ) -@given(st.sampled_from(YEARS), st.lists(couples(), min_size=8, max_size=20)) +@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=15)) def test_claimed_spouse_does_not_depend_on_head_label(year, units): _check_swap(units, year, TAX_UNIT_OUTPUTS + CONSUMERS) @pytest.mark.parametrize("year", YEARS) def test_seeded_population(year): - units = _seeded_couples(AFFECTED, per_state=4) + _crafted_cases(AFFECTED) + units = _seeded_couples(AFFECTED, per_state=2) + _crafted_cases(AFFECTED) _check_swap(units, year, TAX_UNIT_OUTPUTS + CONSUMERS) _check_monotone(units, year) diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.yaml new file mode 100644 index 00000000000..2b934fda74a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.yaml @@ -0,0 +1,89 @@ +- name: Only the head is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [head, spouse] + output: + every_filer_is_dependent_elsewhere: false + +- name: Only the spouse is claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + output: + every_filer_is_dependent_elsewhere: false + +- name: Both spouses are claimed elsewhere + period: 2025 + input: + people: + head: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + spouse: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + output: + every_filer_is_dependent_elsewhere: true + +- name: Single filer claimed elsewhere + period: 2025 + input: + people: + head: + age: 19 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head] + output: + every_filer_is_dependent_elsewhere: true + +- name: A tax unit with neither a head nor a spouse has no filer claimed elsewhere + period: 2025 + input: + people: + child: + age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [child] + output: + every_filer_is_dependent_elsewhere: false diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py index db97f6a626f..61f4c81eaf2 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py @@ -18,14 +18,12 @@ def formula(tax_unit, period, parameters): # cannot be claimed as a dependent. Schedule X stops at line 3 only # for the first-listed filer; since a couple may list either spouse # first, a joint return qualifies unless both spouses can be claimed. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) p = parameters(period).gov.states.hi.tax.income.credits.lihrtc agi = tax_unit("hi_agi", period) rent = add(tax_unit, period, ["rent"]) return ( (rent > p.eligibility.rent_threshold) & (agi < p.eligibility.agi_limit) - & independent_filer + & ~every_filer_dependent ) diff --git a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py index d761c2d8ecf..6d8e7aac8dd 100644 --- a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py +++ b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py @@ -18,9 +18,7 @@ def formula(tax_unit, period, parameters): # individual the base credit for the return's filing status. On a # joint return where only one spouse can be claimed, the other spouse # still qualifies for the joint credit. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) filing_status = tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE - return independent_filer & ~separate + return ~every_filer_dependent & ~separate diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py index 48198e1b904..3bdf5fa4fed 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/cdcc/nm_cdcc_eligible.py @@ -19,9 +19,7 @@ def formula(tax_unit, period, parameters): # taxpayer, your spouse may still qualify to claim rebates or # credits" (PIT-RC instructions). So only a return on which every # filer is a dependent elsewhere is barred. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) p = parameters(period).gov.states.nm.tax.income.credits.cdcc # Filer has to be be gainfully employed to receive credit has_earnings = person("earned_income", period) > 0 @@ -52,5 +50,8 @@ def formula(tax_unit, period, parameters): ) income_eligible = nm_modified_gross_income <= income_limit return ( - independent_filer & employment_eligible & ~receives_tanf & income_eligible + ~every_filer_dependent + & employment_eligible + & ~receives_tanf + & income_eligible ) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py index 205f3b1b0e4..27585ed2568 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py @@ -25,10 +25,8 @@ def formula(tax_unit, period, parameters): # not a dependent of another taxpayer, your spouse may still qualify # to claim rebates or credits" (PIT-RC instructions). So only a return # on which every filer is a dependent elsewhere is barred. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) - eligible = age_eligible & expense_eligible & independent_filer + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) + eligible = age_eligible & expense_eligible & ~every_filer_dependent # exemption is halved for married filing separately filing_status = tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE diff --git a/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py b/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py index d66ef23f1a6..389227abe70 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py @@ -18,10 +18,8 @@ def formula(tax_unit, period, parameters): # instructions let a spouse who is not a dependent still qualify. So # only a return on which every filer is a dependent elsewhere is # barred. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) # deduction does not apply if an exemption under IRS 151 is claimed; # IRC 151 refers to the federal personal exemption federal_exemption_amount = tax_unit("exemptions", period) - return independent_filer & (federal_exemption_amount == 0) + return ~every_filer_dependent & (federal_exemption_amount == 0) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py index 8ce48277203..c49035340e2 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py @@ -19,13 +19,11 @@ def formula(tax_unit, period, parameters): # not a dependent of another taxpayer, your spouse may still qualify # to claim rebates or credits" (PIT-RC instructions). So only a return # on which every filer is a dependent elsewhere is barred. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) agi = tax_unit("adjusted_gross_income", period) filing_status = tax_unit("filing_status", period) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] income_eligible = agi < p.main.income_limit[filing_status] - eligible = income_eligible & independent_filer + eligible = income_eligible & ~every_filer_dependent amount_if_eligible = p.main.amount[filing_status] return eligible * amount_if_eligible diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py index dd68e862de4..f574c92dbab 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py @@ -19,9 +19,7 @@ def formula(tax_unit, period, parameters): # not a dependent of another taxpayer, your spouse may still qualify # to claim rebates or credits" (PIT-RC instructions). So only a return # on which every filer is a dependent elsewhere is barred. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] filing_status = tax_unit("filing_status", period) - return independent_filer * p.additional.amount[filing_status] + return ~every_filer_dependent * p.additional.amount[filing_status] diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py index e98e3be36dd..bf8f1a88d52 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py @@ -19,9 +19,7 @@ def formula(tax_unit, period, parameters): # not a dependent of another taxpayer, your spouse may still qualify # to claim rebates or credits" (PIT-RC instructions). So only a return # on which every filer is a dependent elsewhere is barred. - independent_filer = ( - tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 - ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] filing_status = tax_unit("filing_status", period) - return independent_filer * p.supplemental.amount[filing_status] + return ~every_filer_dependent * p.supplemental.amount[filing_status] diff --git a/policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py new file mode 100644 index 00000000000..161339f0358 --- /dev/null +++ b/policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class every_filer_is_dependent_elsewhere(Variable): + value_type = bool + entity = TaxUnit + definition_period = YEAR + label = "Every head and spouse is a dependent elsewhere" + documentation = ( + "Whether the tax unit has a head or spouse claimed as a dependent in " + "another tax unit and none who is not. False for a tax unit with " + "neither a head nor a spouse." + ) + + def formula(tax_unit, period, parameters): + return tax_unit("head_or_spouse_is_dependent_elsewhere", period) & ( + tax_unit("head_spouse_count_not_dependent_elsewhere", period) == 0 + ) From 8da831289dabf23c4f36e06fa567f4a32a7293d4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 09:14:47 -0400 Subject: [PATCH 03/32] Address review: Hawaii worksheet earnings, NM same-filer age, childless EITC - Hawaii's dependent standard deduction uses earned income net of the deductible part of self-employment tax, as the N-11 worksheet does. - New Mexico's medical care credit needs a filer who is both 65 or older and not a dependent when a filer is a dependent elsewhere (NMSA 7-2-18.13). - The federal EITC without a qualifying child requires that no filer can be claimed as a dependent (IRC 32(c)(1)(A)(ii)(III), Publication 596), so Missouri's working family credit no longer reaches such a couple. - Hawaii's food/excise credit keeps minor children receiving public support (HRS 235-55.85(c)) on a joint return where one spouse can be claimed. - Comments label the readings the law does not settle, and the property test covers the EITC and the food/excise exemption amount. Co-Authored-By: Claude Opus 5.5 --- .../head-dependent-elsewhere-swap.fixed.md | 2 +- ...t_elsewhere_head_spouse_swap_invariance.py | 36 +++-- .../eitc_demographic_eligible.yaml | 130 ++++++++++++++++++ .../food_excise/hi_food_excise_credit.yaml | 130 ++++++++++++++++++ .../tax/income/deductions/hi_deductions.yaml | 79 +++++++++++ .../income/credits/wftc/mo_wftc_eligible.yaml | 62 +++++++++ .../credits/nm_medical_expense_credit.yaml | 106 ++++++++++++++ .../eligibility/eitc_demographic_eligible.py | 16 ++- .../food_excise/hi_food_excise_credit.py | 11 +- .../hi_food_excise_exemption_amount.py | 20 ++- .../tax/income/credits/hi_act_115_rebate.py | 7 +- ...low_income_household_renters_tax_credit.py | 5 +- .../hi/tax/income/deductions/hi_deductions.py | 7 +- .../me_sales_tax_fairness_credit_eligible.py | 6 +- .../mi/tax/income/exemptions/mi_exemptions.py | 12 +- .../credits/nm_medical_expense_credit.py | 22 +-- ...duction_for_certain_dependents_eligible.py | 9 +- .../2021_rebate/nm_2021_income_rebate.py | 13 +- .../nm_additional_2021_income_rebate.py | 13 +- .../nm_supplemental_2021_income_rebate.py | 13 +- .../va_low_income_tax_credit_eligible.py | 9 +- 21 files changed, 635 insertions(+), 73 deletions(-) diff --git a/changelog.d/head-dependent-elsewhere-swap.fixed.md b/changelog.d/head-dependent-elsewhere-swap.fixed.md index 4b4d878f15f..42a32945776 100644 --- a/changelog.d/head-dependent-elsewhere-swap.fixed.md +++ b/changelog.d/head-dependent-elsewhere-swap.fixed.md @@ -1 +1 @@ -The federal standard deduction and the New York, Michigan, Oregon, Virginia, Missouri, New Mexico, Maine and Hawaii rules for a filer claimed as a dependent elsewhere no longer depend on which spouse is labelled head; each follows its jurisdiction's rule for a joint return where one spouse can be claimed. New York's $3,100 dependent standard deduction now applies only to single filers, and Hawaii now limits the standard deduction of a filer who can be claimed as a dependent. +The federal standard deduction and the New York, Michigan, Oregon, Virginia, Missouri, New Mexico, Maine and Hawaii rules for a filer claimed as a dependent elsewhere no longer depend on which spouse is labelled head; each follows its jurisdiction's rule for a joint return where one spouse can be claimed. New York's $3,100 dependent standard deduction now applies only to single filers; Hawaii now limits the standard deduction of a filer who can be claimed as a dependent; the federal EITC without a qualifying child now requires that no filer can be claimed as a dependent; and New Mexico's medical care credit requires a filer who is both 65 or older and not a dependent when a filer is a dependent elsewhere. diff --git a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py index 721d38dff05..d64c56ff5ba 100644 --- a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py +++ b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py @@ -4,34 +4,40 @@ A married couple may list either spouse first, so exchanging the two adults' `is_tax_unit_head` and `is_tax_unit_spouse` labels, with every other input kept, must not change their taxes. On main, the federal standard deduction and -seventeen state formulas read only `head_is_dependent_elsewhere`, so a joint -return where one spouse can be claimed on another return (a joint return filed -only to recover withholding can meet the federal exception, IRS Publication -501) was taxed differently depending on which spouse was labelled head. Each +16 state formulas read only `head_is_dependent_elsewhere`, so a joint return +where one spouse can be claimed on another return (a joint return filed only +to recover withholding can meet the federal exception, IRS Publication 501) +was taxed differently depending on which spouse was labelled head. Each formula now applies its jurisdiction's rule for that case: - Either spouse limits the return (the IRC 63(c)(5) worksheet): the federal standard deduction, Oregon's and Hawaii's standard deductions, and - Virginia's credit for low-income individuals. + Virginia's credit for low-income individuals. The federal EITC without a + qualifying child now also requires that neither spouse can be claimed. - Only single filers: New York's dependent standard deduction. - Each spouse separately: Michigan's exemptions and Hawaii's food/excise, renters and Act 115 credits count only the filers who cannot be claimed. - Only when every filer is a dependent: New Mexico's rebates, credits and - dependents deduction, and Maine's sales tax fairness credit. -- Never on a joint return: Missouri's working family credit, since such a - couple files as married filing combined. + dependents deduction, and Maine's sales tax fairness credit. New Mexico's + medical care credit also needs a filer who is both 65 or older and not a + dependent. +- Never on a joint return: Missouri's own stop in its working family credit, + since such a couple files as married filing combined. Hypothesis draws batches of married couples, with and without dependents, in the eight states with such a rule and one without, for 2021-2026, with either, -both or neither spouse claimed elsewhere. A seeded population and crafted -cases add breadth in every year, and two seeded couples plus the review case -in every state guard 2026. Each batch is one vectorized simulation. For each couple: +both or neither spouse claimed elsewhere, and checks 1 and 3. A seeded +population and crafted cases check 1, 2 and 3 in every year, and two seeded +couples plus the review case in every state check 1 for federal and state +income tax in 2026. Each batch is one vectorized simulation. For each couple: 1. Swap invariance: federal income tax, state income tax before and after - refundable credits, and each of the eighteen formulas are the same to the - cent (or the same boolean) under either labelling. + refundable credits, and each formula in CONSUMERS are the same to the cent + (or the same boolean) under either labelling. 2. Monotonicity: marking another filer as claimed elsewhere never raises any - of the eighteen amounts or turns an eligibility on. + amount in CONSUMERS or turns an eligibility on. This holds for current + parameters and inputs the model derives; for example, a Hawaii standard + deduction input below the itemization threshold could break it. 3. The helper identities: `head_or_spouse_is_dependent_elsewhere` equals `head_is_dependent_elsewhere | spouse_is_dependent_elsewhere`, `every_filer_is_dependent_elsewhere` equals their conjunction for a @@ -59,6 +65,7 @@ # The formulas that read whether a filer is claimed elsewhere. CONSUMERS = [ "basic_standard_deduction", + "eitc", "ny_standard_deduction", "mi_exemptions", "or_standard_deduction", @@ -73,6 +80,7 @@ "me_sales_tax_fairness_credit_eligible", "hi_deductions", "hi_act_115_rebate", + "hi_food_excise_exemption_amount", "hi_food_excise_credit", "hi_tax_credit_for_low_income_household_renters_eligible", "hi_tax_credit_for_low_income_household_renters", diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml index ae065070ccc..80b379d010b 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml @@ -127,3 +127,133 @@ is_full_time_student: true output: eitc_demographic_eligible: false + +- name: 2025 childless single filer who can be claimed as a dependent is ineligible + period: 2025 + input: + people: + head: + age: 30 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head] + output: + # IRC 32(c)(1)(A)(ii)(III). + eitc_demographic_eligible: false + +- name: 2025 childless joint return where one spouse can be claimed is ineligible (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: CA + output: + # Publication 596 (2025, p.18): "neither you nor your spouse can be claimed as a + # dependent by another person if you claim the EIC on your joint return." + eitc_demographic_eligible: false + +- name: 2025 childless joint return where one spouse can be claimed is ineligible (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: CA + output: + # Publication 596 (2025, p.18): "neither you nor your spouse can be claimed as a + # dependent by another person if you claim the EIC on your joint return." + eitc_demographic_eligible: false + +- name: 2025 joint return with a child where one spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child_1] + state_code: CA + output: + # The dependent condition in IRC 32(c)(1)(A)(ii)(III) applies only without a + # qualifying child. + eitc_demographic_eligible: true + +- name: 2025 joint return with a child where one spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child_1] + state_code: CA + output: + # The dependent condition in IRC 32(c)(1)(A)(ii)(III) applies only without a + # qualifying child. + eitc_demographic_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml index e340975fe5c..eaf2ffad6cd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml @@ -331,3 +331,133 @@ state_code: HI output: hi_food_excise_credit: 0 + +- name: 2022 joint return with a publicly supported minor where only one spouse can be claimed (the claimable spouse is the head) + period: 2022 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + # The spouse who cannot be claimed: $110; HRS 235-55.85(c) lets the minor + # receiving public support count for this credit: $110. + hi_food_excise_credit: 220 + +- name: 2022 joint return with a publicly supported minor where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2022 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + # The spouse who cannot be claimed: $110; HRS 235-55.85(c) lets the minor + # receiving public support count for this credit: $110. + hi_food_excise_credit: 220 + +- name: 2025 joint return with a publicly supported minor where only one spouse can be claimed (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + # From 2023 the minor counts as a qualified exemption: 2 x $220. + hi_food_excise_credit: 440 + +- name: 2025 joint return with a publicly supported minor where only one spouse can be claimed (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + # From 2023 the minor counts as a qualified exemption: 2 x $220. + hi_food_excise_credit: 440 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml index 377d8edd769..584d987cbe7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml @@ -195,3 +195,82 @@ output: # HRS 235-2.4(a)(3): greater of $500 or earned income ($300). hi_deductions: 500 + +- name: 2025 single dependent filer with self-employment income uses earned income net of the self-employment tax deduction + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 20 + self_employment_income: 3_000 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + hi_itemized_deductions: 0 + households: + household: + members: [head] + state_code: HI + output: + # N-11 worksheet earned income subtracts Schedule 1 line 15: + # $3,000 x 0.9235 x 15.3% / 2 = $211.94, so $3,000 - $211.94 = $2,788.06. + hi_deductions: 2_788.06 + +- name: 2025 joint return with a claimable self-employed spouse uses earned income net of the self-employment tax deduction (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + self_employment_income: 3_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + hi_itemized_deductions: 0 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + # $3,000 - $3,000 x 0.9235 x 15.3% / 2 = $2,788.06, below the $8,800 joint amount. + hi_deductions: 2788.06 + +- name: 2025 joint return with a claimable self-employed spouse uses earned income net of the self-employment tax deduction (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + self_employment_income: 3_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + hi_itemized_deductions: 0 + households: + household: + members: [person_b, person_a] + state_code: HI + output: + # $3,000 - $3,000 x 0.9235 x 15.3% / 2 = $2,788.06, below the $8,800 joint amount. + hi_deductions: 2788.06 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml index 6cb8a3e48c6..0cea1d97f45 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/credits/wftc/mo_wftc_eligible.yaml @@ -189,3 +189,65 @@ # A joint federal filer whose spouse can be claimed still files Form MO-1040 # as married filing combined, not as claimed as a dependent. mo_wftc_eligible: true + +- name: 2025 childless couple with a claimable student spouse gets no federal EITC and so no WFTC (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + is_full_time_student: true + employment_income: 8_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + employment_income: 8_000 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: MO + output: + # Without a qualifying child, neither spouse may be claimable for the federal + # EITC (Publication 596), which RSMo 143.177 requires. + eitc: 0 + mo_wftc_eligible: false + +- name: 2025 childless couple with a claimable student spouse gets no federal EITC and so no WFTC (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + employment_income: 8_000 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + is_full_time_student: true + employment_income: 8_000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: MO + output: + # Without a qualifying child, neither spouse may be claimable for the federal + # EITC (Publication 596), which RSMo 143.177 requires. + eitc: 0 + mo_wftc_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml index 518854ff24e..f8563d24439 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml @@ -143,3 +143,109 @@ state_code: NM output: nm_medical_expense_credit: 0 + +- name: Joint return where only the claimable spouse is 65 or older (the claimable spouse is the head) + period: 2023 + input: + people: + person_a: + age: 70 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 60 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + itemized_medical_expenses: 28_000 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # NMSA 7-2-18.13(A): the taxpayer who is 65 or older must also not be a dependent. + nm_medical_expense_credit: 0 + +- name: Joint return where only the claimable spouse is 65 or older (the claimable spouse is the spouse) + period: 2023 + input: + people: + person_b: + age: 60 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 70 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + itemized_medical_expenses: 28_000 + households: + household: + members: [person_b, person_a] + state_code: NM + output: + # NMSA 7-2-18.13(A): the taxpayer who is 65 or older must also not be a dependent. + nm_medical_expense_credit: 0 + +- name: Joint return where the spouse who is not claimable is 65 or older (the claimable spouse is the head) + period: 2023 + input: + people: + person_a: + age: 60 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 70 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + itemized_medical_expenses: 28_000 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + nm_medical_expense_credit: 2_800 + +- name: Joint return where the spouse who is not claimable is 65 or older (the claimable spouse is the spouse) + period: 2023 + input: + people: + person_b: + age: 70 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 60 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + itemized_medical_expenses: 28_000 + households: + household: + members: [person_b, person_a] + state_code: NM + output: + nm_medical_expense_credit: 2_800 diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py index 2338356763e..053ca2799c0 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py @@ -6,7 +6,10 @@ class eitc_demographic_eligible(Variable): entity = TaxUnit label = "Meets demographic eligibility for EITC" definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/32#c_1_A_ii_II" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/32#c_1_A_ii", + "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=18", + ) def formula(tax_unit, period, parameters): person = tax_unit.members @@ -27,4 +30,13 @@ def formula(tax_unit, period, parameters): # IRC § 32(c)(1)(A)(ii)(II) applies the age test to the filer or, # on a joint return, either spouse — never to dependents. is_filer_or_spouse = ~person("is_tax_unit_dependent", period) - return has_child | tax_unit.any(meets_age_requirements & is_filer_or_spouse) + # IRC § 32(c)(1)(A)(ii)(III): without a qualifying child, the filer + # must not be a dependent of another taxpayer; on a joint return, + # "neither you nor your spouse can be claimed as a dependent by another + # person" (IRS Publication 596). + dependent_elsewhere = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + childless_eligible = ( + tax_unit.any(meets_age_requirements & is_filer_or_spouse) + & ~dependent_elsewhere + ) + return has_child | childless_eligible diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py index a98403539f7..220561807b8 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py @@ -15,13 +15,14 @@ class hi_food_excise_credit(Variable): def formula(tax_unit, period, parameters): # HRS 235-55.85(a): only a taxpayer who cannot be claimed as a - # dependent may claim the credit, for each qualified exemption. The + # dependent may claim the credit, for each qualified exemption; the # exemption amount counts only filers who cannot be claimed when - # either can; such a return claims no dependents (IRS Publication - # 501), so it gets no minor child amount. - dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + # either can. Minor children receiving public support count for this + # credit under 235-55.85(c), so they keep their amount unless every + # filer can be claimed. + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) exemption_amount = tax_unit("hi_food_excise_exemption_amount", period) minor_child_amount = tax_unit( "hi_food_excise_credit_minor_child_amount", period ) - return exemption_amount + where(dependent_filer, 0, minor_child_amount) + return exemption_amount + where(every_filer_dependent, 0, minor_child_amount) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py index d11bb0a6cac..c597ba1352b 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py @@ -44,21 +44,29 @@ def formula(tax_unit, period, parameters): ], ) exemptions = tax_unit("exemptions_count", period) + minor_children = tax_unit("hi_food_excise_credit_minor_child_count", period) if p.minor_child.in_effect: - # Reduce number of exemptions by the number of minor children - minor_children = tax_unit("hi_food_excise_credit_minor_child_count", period) + # Reduce number of exemptions by the number of minor children, + # who get a fixed amount instead. claimable_exemptions = exemptions - minor_children + minor_child_exemptions = 0 else: claimable_exemptions = exemptions + minor_child_exemptions = minor_children # Form N-311 counts only people who cannot be claimed as a dependent - # by another taxpayer. A return on which a filer can be claimed claims - # no dependents (IRS Publication 501), so it counts only the filers - # who cannot be claimed. + # by another taxpayer. A return on which a filer can be claimed + # generally claims no dependents (IRS Publication 501; its exception + # for a claimer who files only for a refund is not modeled), so it + # counts the filers who cannot be claimed and, through HRS + # 235-55.85(c), their minor children receiving public support. dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period ) + mixed_return_exemptions = independent_filers + where( + independent_filers > 0, minor_child_exemptions, 0 + ) qualified_exemptions = where( - dependent_filer, independent_filers, claimable_exemptions + dependent_filer, mixed_return_exemptions, claimable_exemptions ) return qualified_exemptions * amount_per_exemption diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py index 3a229d50ebb..734e6305b38 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py @@ -43,9 +43,10 @@ def formula(tax_unit, period, parameters): # not a qualifying resident taxpayer, and each qualifying resident # taxpayer claims the refund for the exemptions they are entitled to. # A filer who can be claimed has no exemption (HRS 235-54(a)), and a - # return on which a filer can be claimed claims no dependents (IRS - # Publication 501), so such a return counts only the filers who - # cannot be claimed. + # return on which a filer can be claimed generally claims no dependents + # (IRS Publication 501; its exception for a claimer who files only for + # a refund is not modeled), so such a return counts only the filers + # who cannot be claimed. dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py index 81a87e2876e..f60088206d8 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py @@ -19,8 +19,9 @@ def formula(tax_unit, period, parameters): # Schedule X lines 8-10 count only people who cannot be claimed as a # dependent by another taxpayer, with an extra exemption for each # such filer aged 65 or over. A return on which a filer can be - # claimed claims no dependents (IRS Publication 501), so it counts - # only the filers who cannot be claimed. + # claimed generally claims no dependents (IRS Publication 501; its + # exception for a claimer who files only for a refund is not modeled), + # so it counts only the filers who cannot be claimed. aged_head = (tax_unit("age_head", period) >= p.aged_age_threshold) & ~tax_unit( "head_is_dependent_elsewhere", period ) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py index 70a783508dd..ee104646ba4 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py @@ -37,8 +37,13 @@ def formula(tax_unit, period, parameters): is_dependent_on_another_return = tax_unit( "head_or_spouse_is_dependent_elsewhere", period ) + # Worksheet earned income: wages and self-employment income minus the + # deductible part of self-employment tax (Schedule 1, line 15). + worksheet_earned_income = max_( + tax_unit_earned_income - tax_unit("self_employment_tax_ald", period), 0 + ) dependent_standard_deduction = min_( - max_(p.threshold.dependent, tax_unit_earned_income), standard_deduction + max_(p.threshold.dependent, worksheet_earned_income), standard_deduction ) applicable_standard_deduction = where( is_dependent_on_another_return, diff --git a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py index 6d8e7aac8dd..cf13c6de8ea 100644 --- a/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py +++ b/policyengine_us/variables/gov/states/me/tax/income/credits/fairness/sales_tax_fairness_credit/me_sales_tax_fairness_credit_eligible.py @@ -15,9 +15,9 @@ class me_sales_tax_fairness_credit_eligible(Variable): def formula(tax_unit, period, parameters): # 36 MRSA 5213-A(6)(C) excludes individuals who may be claimed as a # dependent on another return, and subsection 2 allows each resident - # individual the base credit for the return's filing status. On a - # joint return where only one spouse can be claimed, the other spouse - # still qualifies for the joint credit. + # individual the base credit for the return's filing status. Neither + # settles a joint return where only one spouse can be claimed; we read + # them as letting the other spouse qualify for the joint credit. every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) filing_status = tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE diff --git a/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py b/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py index 1c77fb504ce..1940d6d1af8 100644 --- a/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py +++ b/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py @@ -35,11 +35,13 @@ def formula(tax_unit, period, parameters): # MCL 206.30(4): an individual who can be claimed as a dependent on # another return gets no personal exemption but subtracts $1,500. - # This applies to each spouse separately, so on a joint return where - # only one spouse can be claimed, the other keeps their own personal - # exemption (and any stillbirth exemption). A joint return on which - # either spouse can be claimed claims no dependents (IRS Publication - # 501, "Dependent Taxpayer Test"). + # Treasury's instructions do not address a joint return where only + # one spouse can be claimed; since subsection (4) speaks of an + # individual, we apply it to each spouse, so the other keeps their own + # personal exemption (and any stillbirth exemption). A joint return on + # which either spouse can be claimed generally claims no dependents + # (IRS Publication 501, "Dependent Taxpayer Test"; its exception for a + # claimer who files only for a refund is not modeled). filers = add(tax_unit, period, ["is_tax_unit_head_or_spouse"]) independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py index 27585ed2568..1ee85c8bfae 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py @@ -18,15 +18,21 @@ def formula(tax_unit, period, parameters): p = pcredits.unreimbursed_medical_care_expense age = person("age", period) medical_expense = tax_unit("itemized_medical_expenses", period) - age_eligible = tax_unit.any(age >= p.age_eligibility) + aged = age >= p.age_eligibility expense_eligible = medical_expense >= p.min_expenses - # The rebate and credit schedule bars a filer who is a dependent of - # another taxpayer, but "If you are a dependent with a spouse who was - # not a dependent of another taxpayer, your spouse may still qualify - # to claim rebates or credits" (PIT-RC instructions). So only a return - # on which every filer is a dependent elsewhere is barred. - every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) - eligible = age_eligible & expense_eligible & ~every_filer_dependent + # NMSA 7-2-18.13(A) allows the credit to a taxpayer "who is sixty-five + # years of age or older and who is not a dependent of another + # taxpayer", and the PIT-RC instructions let a spouse who is not a + # dependent claim it when the other spouse is. So when a filer is a + # dependent elsewhere, a filer who is not must be 65 or older. + filer = person("is_tax_unit_head_or_spouse", period) + claimed = person("claimed_as_dependent_on_another_return", period) + independent_aged_filer = tax_unit.any(filer & ~claimed & aged) + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + age_eligible = where( + dependent_filer, independent_aged_filer, tax_unit.any(aged) + ) + eligible = age_eligible & expense_eligible # exemption is halved for married filing separately filing_status = tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE diff --git a/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py b/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py index 389227abe70..c083077aebd 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/deductions/certain_dependents/nm_deduction_for_certain_dependents_eligible.py @@ -14,10 +14,11 @@ class nm_deduction_for_certain_dependents_eligible(Variable): def formula(tax_unit, period, parameters): # The deduction uses the same "not a dependent of another individual" - # test as the rebates and credits on Schedule PIT-RC, whose - # instructions let a spouse who is not a dependent still qualify. So - # only a return on which every filer is a dependent elsewhere is - # barred. + # test as the rebates and credits on Schedule PIT-RC, and the law does + # not settle a joint return where only one spouse is a dependent. We + # apply the PIT-RC instructions' reading, which lets a spouse who is + # not a dependent still qualify, so only a return on which every + # filer is a dependent elsewhere is barred. every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) # deduction does not apply if an exemption under IRS 151 is claimed; # IRC 151 refers to the federal personal exemption diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py index c49035340e2..0dab3d96509 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_2021_income_rebate.py @@ -14,11 +14,14 @@ class nm_2021_income_rebate(Variable): defined_for = StateCode.NM def formula(tax_unit, period, parameters): - # The rebate and credit schedule bars a filer who is a dependent of - # another taxpayer, but "If you are a dependent with a spouse who was - # not a dependent of another taxpayer, your spouse may still qualify - # to claim rebates or credits" (PIT-RC instructions). So only a return - # on which every filer is a dependent elsewhere is barred. + # The rebate goes to "a resident ... who is not a dependent of another + # individual", and the law does not settle a joint return where only + # one spouse is. For the same phrase on its rebate and credit schedule, + # TRD says "If you are a dependent with a spouse who was not a + # dependent of another taxpayer, your spouse may still qualify to + # claim rebates or credits" (PIT-RC instructions); we apply that + # reading, so only a return on which every filer is a dependent + # elsewhere is barred. every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) agi = tax_unit("adjusted_gross_income", period) filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py index f574c92dbab..78493990ef0 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_additional_2021_income_rebate.py @@ -14,11 +14,14 @@ class nm_additional_2021_income_rebate(Variable): defined_for = StateCode.NM def formula(tax_unit, period, parameters): - # The rebate and credit schedule bars a filer who is a dependent of - # another taxpayer, but "If you are a dependent with a spouse who was - # not a dependent of another taxpayer, your spouse may still qualify - # to claim rebates or credits" (PIT-RC instructions). So only a return - # on which every filer is a dependent elsewhere is barred. + # The rebate goes to "a resident ... who is not a dependent of another + # individual", and the law does not settle a joint return where only + # one spouse is. For the same phrase on its rebate and credit schedule, + # TRD says "If you are a dependent with a spouse who was not a + # dependent of another taxpayer, your spouse may still qualify to + # claim rebates or credits" (PIT-RC instructions); we apply that + # reading, so only a return on which every filer is a dependent + # elsewhere is barred. every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py index bf8f1a88d52..cd82e30a49b 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/2021_rebate/nm_supplemental_2021_income_rebate.py @@ -14,11 +14,14 @@ class nm_supplemental_2021_income_rebate(Variable): defined_for = StateCode.NM def formula(tax_unit, period, parameters): - # The rebate and credit schedule bars a filer who is a dependent of - # another taxpayer, but "If you are a dependent with a spouse who was - # not a dependent of another taxpayer, your spouse may still qualify - # to claim rebates or credits" (PIT-RC instructions). So only a return - # on which every filer is a dependent elsewhere is barred. + # The rebate goes to "a resident ... who is not a dependent of another + # individual", and the law does not settle a joint return where only + # one spouse is. For the same phrase on its rebate and credit schedule, + # TRD says "If you are a dependent with a spouse who was not a + # dependent of another taxpayer, your spouse may still qualify to + # claim rebates or credits" (PIT-RC instructions); we apply that + # reading, so only a return on which every filer is a dependent + # elsewhere is barred. every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) p = parameters(period).gov.states.nm.tax.income.rebates["2021_income"] filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py b/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py index 7b746711ad1..58934026c25 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py +++ b/policyengine_us/variables/gov/states/va/tax/income/credits/low_income_credit/va_low_income_tax_credit_eligible.py @@ -18,10 +18,11 @@ def formula(tax_unit, period, parameters): program_eligible = add(tax_unit, period, p.ineligible_programs) == 0 # Criteria 2: Not qualified if a filer is claimed as a dependent on - # another taxpayer's return. Va. Code 58.1-339.8 bars the credit - # against the tax of a dependent of another individual or couple; a - # joint return carries both spouses' tax, so either spouse being - # claimed bars it. + # another taxpayer's return. Neither Va. Code 58.1-339.8 nor the + # instructions settle a joint return where only one spouse is + # claimed. We read the statute's bar on the credit "against such tax + # of a dependent" as reaching a joint return, which carries both + # spouses' tax, so either spouse being claimed bars it. filer_is_dependent_elsewhere = tax_unit( "head_or_spouse_is_dependent_elsewhere", period ) From e8386234d207195fbcc487daa650d584e25a83e2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 10:00:44 -0400 Subject: [PATCH 04/32] Allow a cent of rounding in the Hawaii self-employed joint pair Co-Authored-By: Claude Opus 5.5 --- .../gov/states/hi/tax/income/deductions/hi_deductions.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml index 584d987cbe7..56eb95f2a84 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml @@ -221,6 +221,7 @@ - name: 2025 joint return with a claimable self-employed spouse uses earned income net of the self-employment tax deduction (the claimable spouse is the head) period: 2025 + absolute_error_margin: 0.01 input: people: person_a: @@ -249,6 +250,7 @@ - name: 2025 joint return with a claimable self-employed spouse uses earned income net of the self-employment tax deduction (the claimable spouse is the spouse) period: 2025 + absolute_error_margin: 0.01 input: people: person_b: From 41fb4a55471a77d8a4fd7508d5cd177d802557fd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 10:25:58 -0400 Subject: [PATCH 05/32] Address review round 2: worksheet earnings, minors' own status, input meaning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The federal and Oregon dependent worksheets use earned income net of the deductible part of self-employment tax, as Hawaii's now does. - Hawaii's food/excise minor-child count excludes children who can be claimed by another taxpayer and the filers themselves (Form N-311 line 3). - claimed_as_dependent_on_another_return documents what it stands for: a §151 deduction for the person is allowable to another taxpayer, so a person whose would-be claimer files only for a refund is not one (Publications 501 and 596); rules on actually being claimed read it too. - Hawaii's renters comment labels the mixed joint reading as an inference; the Hawaii dependent threshold cites HRS 235-2.4(a)(3) on its page. Co-Authored-By: Claude Opus 5.5 --- .../itemized/threshold/dependent.yaml | 2 +- ...t_elsewhere_head_spouse_swap_invariance.py | 6 +- .../eitc_demographic_eligible.yaml | 21 +++ .../standard/basic_standard_deduction.yaml | 22 +++ .../food_excise/hi_food_excise_credit.yaml | 134 ++++++++++++++++++ .../standard/or_standard_deduction.yaml | 22 +++ .../basic_standard_deduction.py | 10 +- ...hi_food_excise_credit_minor_child_count.py | 9 +- ...r_low_income_household_renters_eligible.py | 8 +- .../deductions/or_standard_deduction.py | 10 +- .../claimed_as_dependent_on_another_return.py | 10 +- 11 files changed, 241 insertions(+), 13 deletions(-) diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml index 3ff2ac9a451..36300fde6de 100644 --- a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/dependent.yaml @@ -16,6 +16,6 @@ metadata: - title: 2018 Hawaii Resident Income Tax Instructions Itemized Deductions Worksheet href: https://files.hawaii.gov/tax/forms/2018/n11ins.pdf#page=38 - title: Hawaii Income Tax Law, Chapter 235, Section 235-2.4, (a)(3) - href: https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=10 + href: https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=11 - title: 2025 Hawaii N-11 Instructions Itemized Deductions href: https://files.hawaii.gov/tax/forms/current/n11ins.pdf#page=32 diff --git a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py index d64c56ff5ba..4e681240eb5 100644 --- a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py +++ b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py @@ -18,9 +18,9 @@ - Each spouse separately: Michigan's exemptions and Hawaii's food/excise, renters and Act 115 credits count only the filers who cannot be claimed. - Only when every filer is a dependent: New Mexico's rebates, credits and - dependents deduction, and Maine's sales tax fairness credit. New Mexico's - medical care credit also needs a filer who is both 65 or older and not a - dependent. + dependents deduction, and Maine's sales tax fairness credit. When a filer + is a dependent elsewhere, New Mexico's medical care credit also needs a + filer who is both 65 or older and not a dependent. - Never on a joint return: Missouri's own stop in its working family credit, since such a couple files as married filing combined. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml index 80b379d010b..b922b61cb47 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml @@ -257,3 +257,24 @@ # The dependent condition in IRC 32(c)(1)(A)(ii)(III) applies only without a # qualifying child. eitc_demographic_eligible: true + +- name: 2025 childless single filer whose parent files only to claim a refund is not a dependent + period: 2025 + input: + people: + head: + age: 26 + employment_income: 2_000 + claimed_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: CA + output: + # Publication 596 (2025, p.18): the credit is allowed when the person who can claim + # you files only to claim a refund; such a person is not a dependent, so the + # input is false. + eitc_demographic_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml index b9a821293a5..4099214070c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/deductions/standard/basic_standard_deduction.yaml @@ -263,3 +263,25 @@ output: # Joint earned income $16,000 + $450 = $16,450, below the $31,500 joint amount. basic_standard_deduction: 16_450 + +- name: 2025 dependent filer with self-employment income uses earned income net of the self-employment tax deduction + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 20 + self_employment_income: 3_000 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: CA + output: + # Worksheet earned income subtracts Schedule 1 line 15: + # $3,000 - $3,000 x 0.9235 x 15.3% / 2 = $2,788.06; plus $450 = $3,238.06. + basic_standard_deduction: 3238.06 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml index eaf2ffad6cd..260651381eb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml @@ -461,3 +461,137 @@ output: # From 2023 the minor counts as a qualified exemption: 2 x $220. hi_food_excise_credit: 440 + +- name: 2022 joint return where one spouse and the publicly supported minor can be claimed elsewhere (the claimable spouse is the head) + period: 2022 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + # Form N-311 line 3 lists only minors who cannot be claimed by another taxpayer, + # so only the spouse who cannot be claimed counts: $110. + hi_food_excise_credit: 110 + +- name: 2022 joint return where one spouse and the publicly supported minor can be claimed elsewhere (the claimable spouse is the spouse) + period: 2022 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + # Form N-311 line 3 lists only minors who cannot be claimed by another taxpayer, + # so only the spouse who cannot be claimed counts: $110. + hi_food_excise_credit: 110 + +- name: 2025 joint return where one spouse and the publicly supported minor can be claimed elsewhere (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + # Only the spouse who cannot be claimed counts: 1 x $220. + hi_food_excise_credit: 220 + +- name: 2025 joint return where one spouse and the publicly supported minor can be claimed elsewhere (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4_000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + # Only the spouse who cannot be claimed counts: 1 x $220. + hi_food_excise_credit: 220 diff --git a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml index 7818ce120d2..c040dee6619 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/deductions/standard/or_standard_deduction.yaml @@ -191,3 +191,25 @@ output: # max($1,350, $3,000 + $450) = $3,450, below the $5,670 joint amount. or_standard_deduction: 3_450 + +- name: 2025 dependent filer with self-employment income uses earned income net of the self-employment tax deduction + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 20 + self_employment_income: 1_500 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: OR + output: + # $1,500 - $1,500 x 0.9235 x 15.3% / 2 = $1,394.03; plus $450 = $1,844.03, + # above the $1,350 minimum and below the $2,835 single amount. + or_standard_deduction: 1844.03 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py index e8575cdcc7e..952da61a05f 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/standard_deduction/basic_standard_deduction.py @@ -25,11 +25,17 @@ def formula(tax_unit, period, parameters): # can be claimed. dependent_elsewhere = tax_unit("head_or_spouse_is_dependent_elsewhere", period) standard_deduction = p.amount[filing_status] + # Worksheet earned income: wages and self-employment income minus the + # deductible part of self-employment tax (Schedule 1, line 15). + worksheet_earned_income = max_( + tax_unit("tax_unit_earned_income", period) + - tax_unit("self_employment_tax_ald", period), + 0, + ) standard_deduction_if_dependent = min_( standard_deduction, max_( - p.dependent.additional_earned_income - + tax_unit("tax_unit_earned_income", period), + p.dependent.additional_earned_income + worksheet_earned_income, p.dependent.amount, ), ) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py index ba06f9635a3..c4cc41a8d42 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py @@ -16,6 +16,13 @@ def formula(tax_unit, period, parameters): ) is_child = person("is_child", period) minor = person("age", period) < p.minor_child.age_threshold - eligible_minor_child = is_child & minor & public_support_over_half + # Form N-311 line 3 lists minor children who, among other tests, + # "Cannot be claimed as a dependent by another taxpayer"; the filers + # are listed on line 2. + claimed_elsewhere = person("claimed_as_dependent_on_another_return", period) + filer = person("is_tax_unit_head_or_spouse", period) + eligible_minor_child = ( + is_child & minor & public_support_over_half & ~claimed_elsewhere & ~filer + ) return tax_unit.sum(eligible_minor_child) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py index 61f4c81eaf2..e25924271ee 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_tax_credit_for_low_income_household_renters_eligible.py @@ -15,9 +15,11 @@ class hi_tax_credit_for_low_income_household_renters_eligible(Variable): def formula(tax_unit, period, parameters): # HRS 235-55.7(b) allows the credit to each resident taxpayer who - # cannot be claimed as a dependent. Schedule X stops at line 3 only - # for the first-listed filer; since a couple may list either spouse - # first, a joint return qualifies unless both spouses can be claimed. + # cannot be claimed as a dependent, and Schedule X stops at line 3 if + # "you" can be claimed. Neither settles a joint return where only one + # spouse can be claimed; since the credit and its exemption count are + # per person, we read them as letting the other spouse claim it, so a + # joint return qualifies unless both spouses can be claimed. every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) p = parameters(period).gov.states.hi.tax.income.credits.lihrtc agi = tax_unit("hi_agi", period) diff --git a/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py b/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py index eb3c543e6fa..54d2dc18fbc 100644 --- a/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py +++ b/policyengine_us/variables/gov/states/or/tax/income/deductions/or_standard_deduction.py @@ -20,8 +20,14 @@ def formula(tax_unit, period, parameters): # Core deduction based on filing status. filing_status = tax_unit("filing_status", period) initial_core_deduction = p.amount[filing_status] - # Replace if claimable as a dependent. - earned_income = tax_unit("tax_unit_earned_income", period) + # Replace if claimable as a dependent. Earned income is figured as in + # the federal worksheet, net of the deductible part of self-employment + # tax (Schedule 1, line 15). + earned_income = max_( + tax_unit("tax_unit_earned_income", period) + - tax_unit("self_employment_tax_ald", period), + 0, + ) claimable_dep_floor = p.claimable_as_dependent.min claimable_dep_earned_amount = ( earned_income + p.claimable_as_dependent.earned_income_addition diff --git a/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py b/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py index a62ae0c4703..f941fcb3254 100644 --- a/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py +++ b/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py @@ -6,4 +6,12 @@ class claimed_as_dependent_on_another_return(Variable): entity = Person definition_period = YEAR label = "Is claimed as a dependent elsewhere" - documentation = "Whether the person is claimed as a dependent in another tax unit." + documentation = ( + "Whether another taxpayer can claim the person as a dependent, that " + "is, a deduction under IRC section 151 for the person is allowable to " + "another taxpayer, whether or not it is claimed. A person whose " + "would-be claimer is not required to file and files only to claim a " + "refund is not such a dependent (IRS Publications 501 and 596). Rules " + "that turn on actually being claimed also read this input; the model " + "has no separate input for that." + ) From 436805e3e0c3d386e66016e54f69967d37be4a1b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 10:27:15 -0400 Subject: [PATCH 06/32] Merge the Hawaii deductions references after syncing main Co-Authored-By: Claude Opus 5.5 --- .../gov/states/hi/tax/income/deductions/hi_deductions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py index 646fee7433c..fe50a345a3c 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py @@ -6,12 +6,10 @@ class hi_deductions(Variable): entity = TaxUnit label = "Hawaii deductions" unit = USD - reference = ( - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=15", # Itemized Deduction - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=20", # Standard Deduction - ) reference = ( "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=11", + # PDF pages 15 (itemized deduction), 20 (standard deduction) + "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=15", # PDF pages 15, 20 "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=15", ) From 23c6229b2004fdc2a8a370c94399eae50dcaccfe Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 12:03:39 -0400 Subject: [PATCH 07/32] Apply the federal rules for a filer who can be claimed as a dependent A filer who can be claimed on another return is treated as having no dependents (IRC 152(b)(1); on a joint return either spouse counts) and has no personal exemption (IRC 151(d)(2)); one who is claimed gets no education credits (25A(g)(3)), student loan interest deduction (221(c)) or recovery rebate (6428(d)(2), 6428A(d)(2), 6428B(c)(2)). The model read none of these. Now the child tax credit and credit for other dependents, the under-13 child and dependent care qualifying person, exemptions_count and its RI, DE, OH, OR, WI, HI and NM readers, the recovery rebates, the education credits and the student loan interest deduction apply them symmetrically for joint returns. Georgia keeps its own counts; the HI, WI and Treasury exemption reforms follow the corrected count. Co-Authored-By: Claude Opus 5.5 --- changelog.d/claimable-filer-federal.fixed.md | 1 + .../hi_dependent_exemption_reform.py | 24 ++- .../wi_dependent_exemption_reform.py | 23 ++- .../treasury/repeal_dependent_exemptions.py | 10 +- ...test_claimable_filer_federal_invariants.py | 153 ++++++++++++++++++ .../irs/credits/cdcc/is_cdcc_eligible.yaml | 96 +++++++++++ .../ctc/maximum/ctc_qualifying_child.yaml | 103 ++++++++++++ .../ctc_adult_individual_maximum.yaml | 93 +++++++++++ ...gible_for_american_opportunity_credit.yaml | 130 +++++++++++++++ ...eligible_for_lifetime_learning_credit.yaml | 107 ++++++++++++ .../recovery_rebate_credit.yaml | 112 +++++++++++++ .../student_loan_interest_ald_eligible.yaml | 69 ++++++++ .../exemptions/exemptions_count.yaml | 133 +++++++++++++++ .../personal/de_personal_credit_indv.yaml | 51 ++++++ .../credits/personal/de_personal_credits.yaml | 55 +++++++ .../states/ga/tax/income/credits/ga_ctc.yaml | 30 +++- .../income/credits/ga_low_income_credit.yaml | 16 +- .../nm_low_and_middle_income_exemption.yaml | 99 ++++++++++++ ...m_low_income_comprehensive_tax_rebate.yaml | 80 +++++++++ ...e_comprehensive_tax_rebate_exemptions.yaml | 101 ++++++++++++ .../oh_personal_exemptions_eligible.yaml | 54 +++++++ .../tax/income/exemption/ri_exemptions.yaml | 79 +++++++++ .../wi/tax/income/wi_base_exemption.yaml | 76 +++++++++ .../gov/irs/credits/cdcc/cdcc_eligible.py | 17 +- .../ctc/maximum/ctc_qualifying_child.py | 18 ++- .../ctc_adult_individual_maximum.py | 8 + ...ligible_for_american_opportunity_credit.py | 6 + ...s_eligible_for_lifetime_learning_credit.py | 6 + .../rrc_adult_count_with_valid_ssn.py | 8 +- .../rrc_arpa_dependents_with_valid_ssn.py | 8 +- .../student_loan_interest_ald_eligible.py | 8 +- .../exemptions/exemptions_count.py | 28 +++- .../personal/de_personal_credit_indv.py | 25 ++- .../income/credits/ctc/ga_ctc_potential.py | 12 +- .../credits/ga_low_income_credit_potential.py | 5 +- .../nm_low_and_middle_income_exemption.py | 3 + .../nm_low_income_comprehensive_tax_rebate.py | 8 +- ...ome_comprehensive_tax_rebate_exemptions.py | 6 + .../oh_personal_exemptions_eligible_person.py | 9 +- 39 files changed, 1820 insertions(+), 50 deletions(-) create mode 100644 changelog.d/claimable-filer-federal.fixed.md create mode 100644 policyengine_us/tests/core/test_claimable_filer_federal_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml diff --git a/changelog.d/claimable-filer-federal.fixed.md b/changelog.d/claimable-filer-federal.fixed.md new file mode 100644 index 00000000000..545d474e1a6 --- /dev/null +++ b/changelog.d/claimable-filer-federal.fixed.md @@ -0,0 +1 @@ +Apply the federal rules for a filer who can be claimed as a dependent on another return: no child tax credit, credit for other dependents or under-13 care qualifying child on such a return (IRC 24, 21, 152(b)(1)), no personal exemption for that filer (IRC 151(d)(2)) in the exemption count Rhode Island, Delaware, Ohio, Wisconsin, Oregon, Hawaii and New Mexico read, no recovery rebate for that filer, and no education credits or student loan interest deduction on a return with a claimed filer (IRC 25A(g)(3), 221(c)). diff --git a/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py b/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py index c2ec738773b..3349f9a9f31 100644 --- a/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py +++ b/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py @@ -15,7 +15,11 @@ def formula(tax_unit, period, parameters): person = tax_unit.members age = person("age", period) - is_dependent = person("is_tax_unit_dependent", period) + # A return on which the filer (or, if joint, either spouse) can be + # claimed as a dependent has no dependents (IRC 152(b)(1)). + is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) # Apply age limit if in effect if p.age_limit.in_effect: @@ -49,7 +53,11 @@ class hi_older_dependents_count(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members - is_dependent = person("is_tax_unit_dependent", period) + # A return on which the filer (or, if joint, either spouse) can be + # claimed as a dependent has no dependents (IRC 152(b)(1)). + is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) total_dependents = tax_unit.sum(is_dependent) eligible_dependent_exemptions = tax_unit( "hi_eligible_dependents_count", period @@ -66,14 +74,16 @@ class hi_regular_exemptions(Variable): defined_for = StateCode.HI def formula(tax_unit, period, parameters): - exemptions_count = tax_unit("exemptions_count", period) p = parameters(period).gov.states.hi.tax.income.exemptions - # Personal exemptions exclude the dependent portion. Over-age - # dependents fall back to the personal count. - dependents = tax_unit("tax_unit_dependents", period) + # Personal exemptions exclude the dependent portion: the filers' + # own exemptions (none for a filer who can be claimed as a + # dependent). Over-age dependents fall back to the personal count. + own_exemptions = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period + ) older_dependents = tax_unit("hi_older_dependents_count", period) - personal_count = exemptions_count - dependents + older_dependents + personal_count = own_exemptions + older_dependents # Aged heads and spouses get an extra base exemption (preserved # exactly as the baseline computes it). diff --git a/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py b/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py index cf44d6a86f2..85e40eefe7f 100644 --- a/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py +++ b/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py @@ -15,7 +15,11 @@ def formula(tax_unit, period, parameters): person = tax_unit.members age = person("age", period) - is_dependent = person("is_tax_unit_dependent", period) + # A return on which the filer (or, if joint, either spouse) can be + # claimed as a dependent has no dependents (IRC 152(b)(1)). + is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) # Apply age limit if in effect if p.age_limit.in_effect: @@ -49,7 +53,11 @@ class wi_older_dependents_count(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members - is_dependent = person("is_tax_unit_dependent", period) + # A return on which the filer (or, if joint, either spouse) can be + # claimed as a dependent has no dependents (IRC 152(b)(1)). + is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) total_dependents = tax_unit.sum(is_dependent) eligible_dependent_exemptions = tax_unit( "wi_eligible_dependents_count", period @@ -68,12 +76,13 @@ def formula(tax_unit, period, parameters): p_base = parameters(period).gov.states.wi.tax.income # Personal exemptions exclude the dependent portion. - total_exemptions_count = tax_unit("exemptions_count", period) - dependents = tax_unit("tax_unit_dependents", period) - older_dependents = tax_unit("wi_older_dependents_count", period) - personal_exemptions_count = ( - total_exemptions_count - dependents + older_dependents + # These are the filers' own exemptions (none for a filer who can + # be claimed as a dependent) plus over-age dependents. + own_exemptions = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period ) + older_dependents = tax_unit("wi_older_dependents_count", period) + personal_exemptions_count = own_exemptions + older_dependents personal_exemption_amount = ( personal_exemptions_count * p_base.exemption.base ) diff --git a/policyengine_us/reforms/treasury/repeal_dependent_exemptions.py b/policyengine_us/reforms/treasury/repeal_dependent_exemptions.py index ab54afe5e89..fbfd6b94c1e 100644 --- a/policyengine_us/reforms/treasury/repeal_dependent_exemptions.py +++ b/policyengine_us/reforms/treasury/repeal_dependent_exemptions.py @@ -13,7 +13,15 @@ class exemptions_count(Variable): def formula(tax_unit, period, parameters): total_unit_size = tax_unit("tax_unit_size", period) dependents = tax_unit("tax_unit_dependents", period) - return total_unit_size - dependents + # Only the filers' own exemptions remain; a filer who can be + # claimed as a dependent has none (IRC 151(d)(2)). + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + independent_filers = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period + ) + return where( + dependent_filer, independent_filers, total_unit_size - dependents + ) class reform(Reform): def apply(self): diff --git a/policyengine_us/tests/core/test_claimable_filer_federal_invariants.py b/policyengine_us/tests/core/test_claimable_filer_federal_invariants.py new file mode 100644 index 00000000000..58eb58887b4 --- /dev/null +++ b/policyengine_us/tests/core/test_claimable_filer_federal_invariants.py @@ -0,0 +1,153 @@ +"""Federal rules for a filer who can be claimed as a dependent elsewhere. + +A filer who can be claimed as a dependent on another return is treated as +having no dependents (IRC 152(b)(1); on a joint return, if either spouse can be +claimed, "you and your spouse can't claim any dependents", Publication 501) and +has no personal exemption (IRC 151(d)(2)). So: + +- `exemptions_count` counts only the filers who cannot be claimed, plus the + dependents when no filer can be claimed; +- `ctc_qualifying_child` and the credit for other dependents are zero on a + return with a claimable filer; +- a child under 13 is not a child and dependent care qualifying person on such + a return (a disabled person still is, IRC 21(b)(1)(B)); +- recovery rebates leave out a claimable filer and, on such a return, the + dependents. + +For couples drawn by Hypothesis and a seeded population, with either, both or +neither spouse claimed, in 2021-2026: + +1. Swap invariance: each output is the same under either head/spouse + labelling. +2. Monotonicity: marking another filer as claimed never raises any of the + amounts or counts below. +3. Identities: `exemptions_count` equals the filers who cannot be claimed plus + the dependents when no filer can be claimed, and a return with a claimable + filer has no CTC-qualifying child and no credit for other dependents. +""" + +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.tests.core.test_dependent_elsewhere_head_spouse_swap_invariance import ( + CLAIM_PATTERNS, + TOLERANCE, + YEARS, + _calc, + _crafted_cases, + _monotone_variants, + _seeded_couples, + _situation, + _swap_variants, + adults, + dependents, +) + +# Texas has no income tax; the others read the federal exemption count. +STATES = ["TX", "OR", "HI", "NM", "GA", "RI", "DE", "OH", "WI"] +OUTPUTS = [ + "income_tax", + "ctc_maximum", + "ctc_qualifying_children", + "capped_count_cdcc_eligible", + "exemptions_count", + "rrc_arpa", +] +# Amounts that marking another filer as claimed must never raise. +MONOTONE = [ + "ctc_maximum", + "ctc_qualifying_children", + "capped_count_cdcc_eligible", + "exemptions_count", + "rrc_arpa", +] + + +@st.composite +def couples(draw): + return { + "state": draw(st.sampled_from(STATES)), + "adults": [draw(adults()), draw(adults())], + "dependents": draw(st.lists(dependents(), max_size=2)), + "claimed": draw(st.sampled_from(CLAIM_PATTERNS)), + } + + +def check_swap(units, year, outputs, identities=None): + """Each output is the same under either head/spouse labelling.""" + sim = Simulation(situation=_situation(units, year, _swap_variants)) + for name in outputs: + values = _calc(sim, name, year) + 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 " + "exchanged: " + + ", ".join( + f"{units[i]['state']} claimed={units[i]['claimed']} " + f"{drawn[i]:.2f} -> {swapped[i]:.2f}" + for i in np.flatnonzero(differs)[:10] + ) + ) + if identities is not None: + identities(sim, year) + + +def check_monotone(units, year, outputs, identities=None): + """Marking another filer as claimed never raises an output.""" + sim = Simulation(situation=_situation(units, year, _monotone_variants)) + for name in outputs: + values = _calc(sim, name, year).reshape(-1, 4) + nobody, first, second, both = values.T + for more, fewer, label in ( + (first, nobody, "first adult"), + (second, nobody, "second adult"), + (both, first, "both after first"), + (both, second, "both after second"), + ): + rises = more > fewer + TOLERANCE + assert not rises.any(), ( + f"{name} in {year} rises when the {label} is claimed " + "elsewhere: " + + ", ".join( + f"{units[i]['state']} {fewer[i]:.2f} -> {more[i]:.2f}" + for i in np.flatnonzero(rises)[:10] + ) + ) + if identities is not None: + identities(sim, year) + + +def _federal_identities(sim, year): + claimed_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + independent = _calc(sim, "head_spouse_count_not_dependent_elsewhere", year) + dependents_count = _calc(sim, "tax_unit_count_dependents", year) + expected = independent + np.where(claimed_filer, 0, dependents_count) + np.testing.assert_array_equal(_calc(sim, "exemptions_count", year), expected) + assert not _calc(sim, "ctc_qualifying_children", year)[claimed_filer].any() + odc = np.asarray( + sim.calculate("ctc_adult_individual_maximum", year, map_to="tax_unit") + ) + assert not odc[claimed_filer].any() + + +@settings( + max_examples=4, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=15)) +def test_claimable_filer_federal_rules_are_label_free(year, units): + check_swap(units, year, OUTPUTS, _federal_identities) + + +@pytest.mark.parametrize("year", YEARS) +def test_claimable_filer_federal_rules_seeded_population(year): + units = _seeded_couples(STATES, per_state=2) + _crafted_cases(STATES) + check_swap(units, year, OUTPUTS, _federal_identities) + check_monotone(units, year, MONOTONE, _federal_identities) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/is_cdcc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/is_cdcc_eligible.yaml index 9a4d8616b69..1c590c6f3cf 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/is_cdcc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/is_cdcc_eligible.yaml @@ -54,3 +54,99 @@ is_tax_unit_spouse: false output: is_cdcc_eligible: true + +- name: 2025 child under 13 is not a qualifying person when the single filer can be claimed as a dependent + period: 2025 + input: + people: + parent: + age: 20 + claimed_as_dependent_on_another_return: true + child: + age: 5 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # Publication 503: "Your qualifying child who is your dependent and who + # was under age 13"; IRC 152(b)(1). + is_cdcc_eligible: [false, false] + +- name: 2025 joint return with one claimable spouse has no under-13 qualifying person (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + is_cdcc_eligible: [false, false, false] + +- name: 2025 joint return with one claimable spouse has no under-13 qualifying person (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + is_cdcc_eligible: [false, false, false] + +- name: 2025 disabled person still qualifies when the filer can be claimed as a dependent + period: 2025 + input: + people: + filer: + age: 30 + claimed_as_dependent_on_another_return: true + relative: + age: 60 + is_incapable_of_self_care: true + tax_units: + tax_unit: + members: [filer, relative] + households: + household: + members: [filer, relative] + state_code: TX + output: + # IRC 21(b)(1)(B) determines dependency "without regard to subsections + # (b)(1), (b)(2), and (d)(1)(B)" of section 152; Publication 503 item + # 3(b)(iii). + is_cdcc_eligible: [false, true] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml index 3f4a66e3619..0a40c086281 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml @@ -56,3 +56,106 @@ members: [adult, child, child2] output: ctc_qualifying_child: [False, True, False] + +- name: 2025 single filer who can be claimed as a dependent has no CTC-qualifying child + period: 2025 + input: + people: + parent: + age: 20 + employment_income: 5_000 + claimed_as_dependent_on_another_return: true + child: + age: 5 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # IRC 24(a), 152(b)(1); Publication 501, Dependent Taxpayer Test. + ctc_qualifying_child: [false, false] + ctc_maximum: 0 + +- name: 2025 joint return with one claimable spouse has no CTC-qualifying child (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # "If you are filing a joint return and your spouse can be claimed as a + # dependent by another taxpayer, you and your spouse can't claim any + # dependents on your joint return" (Publication 501). + ctc_qualifying_child: [false, false, false] + ctc_maximum: 0 + +- name: 2025 joint return with one claimable spouse has no CTC-qualifying child (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + ctc_qualifying_child: [false, false, false] + ctc_maximum: 0 + +- name: 2025 joint return with neither spouse claimable keeps the CTC-qualifying child + period: 2025 + input: + people: + person_a: + age: 20 + person_b: + age: 20 + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + ctc_qualifying_child: [false, false, true] + ctc_maximum: 2_200 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml index fec059bb2d7..9c46915a88e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml @@ -43,3 +43,96 @@ ctc_child_individual_maximum: 0 output: ctc_adult_individual_maximum: 500 + +- name: 2025 single filer who can be claimed as a dependent gets no credit for other dependents + period: 2025 + input: + people: + filer: + age: 22 + claimed_as_dependent_on_another_return: true + sibling: + age: 18 + tax_units: + tax_unit: + members: [filer, sibling] + households: + household: + members: [filer, sibling] + state_code: TX + output: + # IRC 24(h)(4) "any dependent of the taxpayer"; IRC 152(b)(1). + ctc_adult_individual_maximum: [0, 0] + +- name: 2025 joint return with one claimable spouse gets no credit for other dependents (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 22 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 22 + is_tax_unit_head: false + is_tax_unit_spouse: true + sibling: + age: 18 + tax_units: + tax_unit: + members: [person_a, person_b, sibling] + filing_status: JOINT + households: + household: + members: [person_a, person_b, sibling] + state_code: TX + output: + ctc_adult_individual_maximum: [0, 0, 0] + +- name: 2025 joint return with one claimable spouse gets no credit for other dependents (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 22 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 22 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + sibling: + age: 18 + tax_units: + tax_unit: + members: [person_b, person_a, sibling] + filing_status: JOINT + households: + household: + members: [person_b, person_a, sibling] + state_code: TX + output: + ctc_adult_individual_maximum: [0, 0, 0] + +- name: 2025 joint return with neither spouse claimable gets the credit for other dependents + period: 2025 + input: + people: + person_a: + age: 22 + person_b: + age: 22 + sibling: + age: 18 + tax_units: + tax_unit: + members: [person_a, person_b, sibling] + filing_status: JOINT + households: + household: + members: [person_a, person_b, sibling] + state_code: TX + output: + ctc_adult_individual_maximum: [0, 0, 500] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml index 0d102e18b78..908d3d3c657 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml @@ -177,3 +177,133 @@ has_tin: true output: is_eligible_for_american_opportunity_credit: false + + +- name: 2024 student claimed as a dependent elsewhere cannot claim the credit + period: 2024 + input: + people: + student: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [student] + households: + household: + members: [student] + state_code: TX + output: + # IRC 25A(g)(3); Form 8863: "You're claimed as a dependent on another + # person's tax return". + is_eligible_for_american_opportunity_credit: false + +- name: 2024 joint return with one spouse claimed elsewhere cannot claim the credit for either spouse (the claimable spouse is the head) + period: 2024 + input: + people: + person_a: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # Form 1040 instructions: "or your spouse if filing jointly". + is_eligible_for_american_opportunity_credit: [false, false] + +- name: 2024 joint return with one spouse claimed elsewhere cannot claim the credit for either spouse (the claimable spouse is the spouse) + period: 2024 + input: + people: + person_b: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [false, false] + +- name: 2024 joint return with neither spouse claimed keeps both students' eligibility + period: 2024 + input: + people: + person_a: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + person_b: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [true, true] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/lifetime_learning_credit/is_eligible_for_lifetime_learning_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/lifetime_learning_credit/is_eligible_for_lifetime_learning_credit.yaml index 72ca159a702..4bff0f55a7d 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/lifetime_learning_credit/is_eligible_for_lifetime_learning_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/lifetime_learning_credit/is_eligible_for_lifetime_learning_credit.yaml @@ -70,3 +70,110 @@ has_tin: true output: is_eligible_for_lifetime_learning_credit: false + + +- name: 2024 student claimed as a dependent elsewhere cannot claim the credit + period: 2024 + input: + people: + student: + age: 30 + attends_eligible_educational_institution_for_lifetime_learning_credit: true + has_lifetime_learning_credit_1098_t_or_exception: true + has_tin: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [student] + households: + household: + members: [student] + state_code: TX + output: + # IRC 25A(g)(3). + is_eligible_for_lifetime_learning_credit: false + +- name: 2024 joint return with one spouse claimed elsewhere cannot claim the credit for either spouse (the claimable spouse is the head) + period: 2024 + input: + people: + person_a: + age: 30 + attends_eligible_educational_institution_for_lifetime_learning_credit: true + has_lifetime_learning_credit_1098_t_or_exception: true + has_tin: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + attends_eligible_educational_institution_for_lifetime_learning_credit: true + has_lifetime_learning_credit_1098_t_or_exception: true + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + is_eligible_for_lifetime_learning_credit: [false, false] + +- name: 2024 joint return with one spouse claimed elsewhere cannot claim the credit for either spouse (the claimable spouse is the spouse) + period: 2024 + input: + people: + person_b: + age: 30 + attends_eligible_educational_institution_for_lifetime_learning_credit: true + has_lifetime_learning_credit_1098_t_or_exception: true + has_tin: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + attends_eligible_educational_institution_for_lifetime_learning_credit: true + has_lifetime_learning_credit_1098_t_or_exception: true + has_tin: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + is_eligible_for_lifetime_learning_credit: [false, false] + +- name: 2024 joint return with neither spouse claimed keeps both students' eligibility + period: 2024 + input: + people: + person_a: + age: 30 + attends_eligible_educational_institution_for_lifetime_learning_credit: true + has_lifetime_learning_credit_1098_t_or_exception: true + has_tin: true + person_b: + age: 30 + attends_eligible_educational_institution_for_lifetime_learning_credit: true + has_lifetime_learning_credit_1098_t_or_exception: true + has_tin: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + is_eligible_for_lifetime_learning_credit: [true, true] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml index e84c1db994a..0d21682feaf 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml @@ -47,3 +47,115 @@ members: [parent, child_1, child_2] output: recovery_rebate_credit: 3_920 # https://www.efile.com/stimulus-check-three/ + + +- name: 2021 single filer who can be claimed as a dependent gets no ARPA rebate + period: 2021 + input: + people: + filer: + age: 20 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: TX + output: + # IRC 6428B(c)(2): an eligible individual excludes "any individual who is a + # dependent of another taxpayer". + rrc_arpa: 0 + +- name: 2021 claimable single filer's child adds no ARPA rebate + period: 2021 + input: + people: + parent: + age: 20 + claimed_as_dependent_on_another_return: true + child: + age: 2 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # IRC 152(b)(1): such a filer has no dependents. + rrc_arpa: 0 + +- name: 2021 joint return with one claimable spouse gets the other spouse's ARPA rebate (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 22 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # Each spouse is an eligible individual or not on their own; $1,400 for + # the spouse who cannot be claimed. + rrc_arpa: 1_400 + +- name: 2021 joint return with one claimable spouse gets the other spouse's ARPA rebate (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 22 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + rrc_arpa: 1_400 + +- name: 2021 joint return with neither spouse claimable gets both rebates and the child's + period: 2021 + input: + people: + person_a: + age: 20 + person_b: + age: 22 + child: + age: 2 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 3 x $1,400. + rrc_arpa: 4_200 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml index ca6f654768e..97566eae089 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml @@ -37,3 +37,72 @@ filing_status: SEPARATE output: student_loan_interest_ald_eligible: false + + +- name: 2024 single filer claimed as a dependent elsewhere cannot deduct student loan interest + period: 2024 + input: + people: + filer: + age: 24 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: TX + output: + # IRC 221(c). + student_loan_interest_ald_eligible: false + +- name: 2024 joint return with one spouse claimed elsewhere cannot deduct student loan interest (the claimable spouse is the head) + period: 2024 + input: + people: + person_a: + age: 24 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 24 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # Form 1040 instructions: "you, or your spouse if filing jointly, aren't + # claimed as a dependent". + student_loan_interest_ald_eligible: [false, false] + +- name: 2024 joint return with one spouse claimed elsewhere cannot deduct student loan interest (the claimable spouse is the spouse) + period: 2024 + input: + people: + person_b: + age: 24 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 24 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + student_loan_interest_ald_eligible: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml new file mode 100644 index 00000000000..16c899b35cf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml @@ -0,0 +1,133 @@ +- name: 2025 single filer with a child counts both + period: 2025 + input: + people: + parent: + age: 30 + child: + age: 5 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + exemptions_count: 2 + +- name: 2025 single filer who can be claimed as a dependent has no exemptions + period: 2025 + input: + people: + parent: + age: 20 + claimed_as_dependent_on_another_return: true + child: + age: 5 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # IRC 151(d)(2) for the filer; IRC 152(b)(1) for the child. + exemptions_count: 0 + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + exemptions_count: 1 + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + exemptions_count: 1 + +- name: 2025 joint return with both spouses claimable has no exemptions + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + person_b: + age: 20 + claimed_as_dependent_on_another_return: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + exemptions_count: 0 + +- name: 2025 joint return with neither spouse claimable counts everyone + period: 2025 + input: + people: + person_a: + age: 20 + person_b: + age: 20 + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + exemptions_count: 3 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 406b0d0f8f9..5964fe8cf76 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 @@ -71,3 +71,54 @@ output: # Equal capacity: split evenly (both columns can absorb their share) de_personal_credit_indv: [110, 110] + + +- name: 2025 spouse who can be claimed as a dependent has no personal credit (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + de_income_tax_before_non_refundable_credits_indv: 1000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + de_income_tax_before_non_refundable_credits_indv: 1000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: DE + output: + # 30 Del. C. 1110(b)(1): $110 for each personal exemption to which the + # individual is entitled for federal purposes; IRC 151(d)(2). + de_personal_credit_indv: [0, 110] + +- name: 2025 spouse who can be claimed as a dependent has no personal credit (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + de_income_tax_before_non_refundable_credits_indv: 1000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + de_income_tax_before_non_refundable_credits_indv: 1000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: DE + output: + de_personal_credit_indv: [110, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credits.yaml index fd12e7c01ce..b13f8f2f86d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credits.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/credits/personal/de_personal_credits.yaml @@ -30,3 +30,58 @@ de_income_tax_before_non_refundable_credits_unit: 1000000 output: de_personal_credit: 0 + + +- name: 2025 joint return with one claimable spouse gets one personal credit (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: DE + output: + # Federal exemptions: the other spouse only; IRC 152(b)(1) leaves no + # dependents. + de_personal_credit_potential: 110 + +- name: 2025 joint return with one claimable spouse gets one personal credit (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: DE + output: + de_personal_credit_potential: 110 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_ctc.yaml index 658c2647ad9..0690ba455ca 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_ctc.yaml @@ -6,7 +6,6 @@ age: 30 child: age: 2 - ctc_qualifying_child: true tax_units: tax_unit: members: @@ -30,7 +29,6 @@ age: 30 child: age: 6 - ctc_qualifying_child: true tax_units: tax_unit: members: @@ -54,10 +52,8 @@ age: 32 child1: age: 1 - ctc_qualifying_child: true child2: age: 5 - ctc_qualifying_child: true tax_units: tax_unit: members: @@ -83,7 +79,6 @@ age: 30 child: age: 2 - ctc_qualifying_child: true tax_units: tax_unit: members: @@ -107,7 +102,6 @@ age: 30 child: age: 2 - ctc_qualifying_child: true tax_units: tax_unit: members: @@ -123,3 +117,27 @@ state_code: GA output: ga_non_refundable_credits: 0 + + +- name: 2026 GA CTC still counts the child of a filer who can be claimed as a dependent + period: 2026 + input: + people: + parent: + age: 20 + claimed_as_dependent_on_another_return: true + child: + age: 2 + tax_units: + tax_unit: + members: [parent, child] + filing_status: SINGLE + ga_income_tax_before_non_refundable_credits: 10000 + households: + household: + members: [parent, child] + state_code: GA + output: + # Georgia uses the IRC 24(c) qualifying-child definition only; whether IRC + # 152(b)(1) reaches it is not settled, so the child test is kept. + ga_ctc: 250 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_low_income_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_low_income_credit.yaml index 5a059239e46..2a410aa94df 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_low_income_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/credits/ga_low_income_credit.yaml @@ -5,7 +5,7 @@ adjusted_gross_income: 14000 age_head: 67 age_spouse: 70 - exemptions_count: 2 + tax_unit_size: 2 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 32 @@ -16,7 +16,7 @@ adjusted_gross_income: 4000 age_head: 67 age_spouse: 56 - exemptions_count: 2 + tax_unit_size: 2 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 78 @@ -27,7 +27,7 @@ adjusted_gross_income: 15500 age_head: 80 age_spouse: 75 - exemptions_count: 3 + tax_unit_size: 3 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 25 @@ -38,7 +38,7 @@ adjusted_gross_income: 4000 age_head: 67 age_spouse: 56 - exemptions_count: 0 + tax_unit_size: 0 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 26 @@ -49,7 +49,7 @@ adjusted_gross_income: 10500 age_head: 67 age_spouse: 56 - exemptions_count: 0 + tax_unit_size: 0 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 8 @@ -60,7 +60,7 @@ adjusted_gross_income: 10500 age_head: 56 age_spouse: 56 - exemptions_count: 0 + tax_unit_size: 0 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 0 @@ -71,7 +71,7 @@ adjusted_gross_income: 10500 age_head: 65 age_spouse: 64 - exemptions_count: 1 + tax_unit_size: 1 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 16 @@ -82,7 +82,7 @@ adjusted_gross_income: 10000 age_head: 64 age_spouse: 64 - exemptions_count: 2 + tax_unit_size: 2 ga_income_tax_before_non_refundable_credits: 1000000 output: ga_low_income_credit: 16 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.yaml index f4acfca8392..5f236216e86 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.yaml @@ -27,3 +27,102 @@ exemptions_count: 3 output: nm_low_and_middle_income_exemption: 4_500 + + +- name: 2025 single filer who can be claimed as a dependent has no exemptions + period: 2025 + input: + people: + filer: + age: 30 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [filer] + adjusted_gross_income: 10_000 + households: + household: + members: [filer] + state_code: NM + output: + # NMSA 7-2-5.8 counts federal exemptions; IRC 151(d)(2). + nm_low_and_middle_income_exemption: 0 + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + adjusted_gross_income: 10_000 + households: + household: + members: [person_a, person_b, child] + state_code: NM + output: + # One exemption (the child is not a dependent, IRC 152(b)(1)) x $2,500. + nm_low_and_middle_income_exemption: 2_500 + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + adjusted_gross_income: 10_000 + households: + household: + members: [person_b, person_a, child] + state_code: NM + output: + # One exemption (the child is not a dependent, IRC 152(b)(1)) x $2,500. + nm_low_and_middle_income_exemption: 2_500 + +- name: 2025 joint return with neither spouse claimable counts everyone + period: 2025 + input: + people: + person_a: + age: 30 + person_b: + age: 30 + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + adjusted_gross_income: 10_000 + households: + household: + members: [person_a, person_b, child] + state_code: NM + output: + # 3 exemptions x $2,500. + nm_low_and_middle_income_exemption: 7_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml index 7bcd28c10fa..6c7e83fb126 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml @@ -246,3 +246,83 @@ state_code: NM output: nm_low_income_comprehensive_tax_rebate: 583 + +- name: 2025 single filer who can be claimed as a dependent gets no rebate + period: 2025 + input: + people: + person: + age: 20 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person] + nm_modified_gross_income: 5_000 + households: + household: + members: [person] + state_code: NM + output: + # NMSA 7-2-14(A): only a resident "who is not a dependent of another + # individual" may claim the rebate. + nm_low_income_comprehensive_tax_rebate: 0 + +- name: 2025 single filer who cannot be claimed gets the one-exemption rebate + period: 2025 + input: + people: + person: + age: 20 + tax_units: + tax_unit: + members: [person] + nm_modified_gross_income: 5_000 + households: + household: + members: [person] + state_code: NM + output: + # $1,000-$7,500 row, one exemption. + nm_low_income_comprehensive_tax_rebate: 252 + +- name: 2025 single filer aged 65 or older who can be claimed as a dependent gets no rebate + period: 2025 + input: + people: + person: + age: 70 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person] + nm_modified_gross_income: 5_000 + households: + household: + members: [person] + state_code: NM + output: + # The age-65 exemptions do not make a dependent of another individual a + # claimant (NMSA 7-2-14(A)). + nm_low_income_comprehensive_tax_rebate: 0 + +- name: 2025 joint return with one claimable spouse keeps the rebate with the couple's combined income + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + person_b: + age: 30 + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + nm_modified_gross_income: 8_500 + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # One exemption; the $8,000-$9,000 row of the one-exemption table. + nm_low_income_comprehensive_tax_rebate: 212 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.yaml index 5b10b5cf2d8..6fa092fcffe 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.yaml @@ -24,3 +24,104 @@ blind_head: true output: nm_low_income_comprehensive_tax_rebate_exemptions: 2 + +- name: 2025 single filer who can be claimed as a dependent has no rebate exemptions + period: 2025 + input: + people: + parent: + age: 20 + claimed_as_dependent_on_another_return: true + child: + age: 2 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NM + output: + # NMSA 7-2-14(A),(C); PIT-RC line 2a. + nm_low_income_comprehensive_tax_rebate_exemptions: 0 + +- name: 2025 joint return with one claimable spouse drops its ordinary exemption and the dependents (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 70 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 40 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 2 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NM + output: + # "the non-qualifying spouse is not eligible" (PIT-RC line 2a) and a + # return on which a filer can be claimed has no dependents (IRC + # 152(b)(1)), leaving 1 ordinary exemption; NMSA 7-2-14(C) still adds 2 + # for "each individual ... included in the return who is sixty-five years + # of age or older". + nm_low_income_comprehensive_tax_rebate_exemptions: 3 + +- name: 2025 joint return with one claimable spouse drops its ordinary exemption and the dependents (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 40 + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 70 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 2 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NM + output: + nm_low_income_comprehensive_tax_rebate_exemptions: 3 + +- name: 2025 joint return with neither spouse claimable counts everyone plus the age-65 exemptions + period: 2025 + input: + people: + person_a: + age: 70 + person_b: + age: 40 + child: + age: 2 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NM + output: + # 3 federal exemptions + 2 for the spouse aged 65 or older. + nm_low_income_comprehensive_tax_rebate_exemptions: 5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml index 97f70fe2376..88f43d6e9af 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml @@ -27,3 +27,57 @@ is_tax_unit_dependent: true output: oh_personal_exemptions_eligible_person: true + + +- name: 2025 joint return with one claimable spouse makes neither that spouse nor the child eligible (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: OH + output: + # Ohio R.C. 5747.025; IRC 152(b)(1) leaves the return no dependents. + oh_personal_exemptions_eligible_person: [false, true, false] + +- name: 2025 joint return with one claimable spouse makes neither that spouse nor the child eligible (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: OH + output: + oh_personal_exemptions_eligible_person: [true, false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/exemption/ri_exemptions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/exemption/ri_exemptions.yaml index 874ee89a624..539a40f00a6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/exemption/ri_exemptions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/exemption/ri_exemptions.yaml @@ -135,3 +135,82 @@ exemptions_count: 2 output: ri_exemptions: 0 + + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + ri_agi: 10000 + households: + household: + members: [person_a, person_b, child] + state_code: RI + output: + # R.I. Gen. Laws 44-30-2.6: "If someone else can claim you on their return, + # your exemption amount is zero"; IRC 152(b)(1). + ri_exemptions: 5_100 + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + ri_agi: 10000 + households: + household: + members: [person_b, person_a, child] + state_code: RI + output: + ri_exemptions: 5_100 + +- name: 2025 joint return with neither spouse claimable counts everyone + period: 2025 + input: + people: + person_a: + age: 30 + person_b: + age: 30 + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + ri_agi: 10000 + households: + household: + members: [person_a, person_b, child] + state_code: RI + output: + ri_exemptions: 15_300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_base_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_base_exemption.yaml index 9e54d8bd87f..f1f8087f317 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_base_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_base_exemption.yaml @@ -5,3 +5,79 @@ state_code: WI output: wi_base_exemption: 5 * 700 + + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: WI + output: + # Form 1 instructions: "either you or your spouse (not both) can be claimed"; + # IRC 152(b)(1). + wi_base_exemption: 700 + +- name: 2025 joint return with one claimable spouse counts only the other spouse (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: WI + output: + wi_base_exemption: 700 + +- name: 2025 joint return with neither spouse claimable counts everyone + period: 2025 + input: + people: + person_a: + age: 30 + person_b: + age: 30 + child: + age: 5 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: WI + output: + wi_base_exemption: 2_100 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py index 82861496784..20d82cc32e0 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py @@ -9,6 +9,9 @@ class is_cdcc_eligible(Variable): reference = ( "https://www.law.cornell.edu/uscode/text/26/21#b_1", "https://www.law.cornell.edu/uscode/text/26/21#e_5", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", + # Publication 503, Who Is a Qualifying Person? + "https://www.irs.gov/pub/irs-prior/p503--2025.pdf#page=3", ) def formula(person, period, parameters): @@ -21,8 +24,18 @@ def formula(person, period, parameters): # qualifying individual even when the noncustodial parent claims the # dependent, so any under-age member other than the head or spouse # qualifies. - qualifies_by_age = ~head_or_spouse & (age < p.child_age) - # Subsections (b)(1)(B) (dependent) and (b)(1)(C) (spouse). + # A child qualifies only as the filer's dependent ("Your qualifying + # child who is your dependent", Publication 503), and under IRC + # 152(b)(1) a return on which the filer (or, if joint, either spouse) + # can be claimed as a dependent has no dependents. + filer_is_dependent = person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) + qualifies_by_age = ~head_or_spouse & (age < p.child_age) & ~filer_is_dependent + # Subsections (b)(1)(B) (dependent) and (b)(1)(C) (spouse). Subsection + # (b)(1)(B) determines dependency "without regard to subsections + # (b)(1), (b)(2), and (d)(1)(B)" of section 152, so a disabled person + # still qualifies when the filer can be claimed as a dependent. disabled = person("is_incapable_of_self_care", period) married = person.tax_unit("tax_unit_married", period) qualifies_by_disability = disabled & (is_dependent | (head_or_spouse & married)) diff --git a/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py b/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py index ae2c1bfab24..bd478b60870 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py @@ -8,7 +8,13 @@ class ctc_qualifying_child(Variable): documentation = "Child qualifies for the Child Tax Credit" definition_period = YEAR defined_for = "is_tax_unit_dependent" - reference = "https://www.law.cornell.edu/uscode/text/26/24#c" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/24#a", + "https://www.law.cornell.edu/uscode/text/26/24#c", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", + # Publication 501, Dependent Taxpayer Test. + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", + ) def formula(person, period, parameters): age = person("age", period) @@ -18,4 +24,12 @@ def formula(person, period, parameters): meets_identification_requirements = person( "meets_ctc_child_identification_requirements", period ) - return age_eligible & meets_identification_requirements + # IRC 24(a) allows the credit only for a qualifying child for whom the + # taxpayer is allowed a deduction under section 151. Under IRC + # 152(b)(1) a taxpayer who can be claimed as a dependent is treated as + # having no dependents, and on a joint return neither spouse may be + # claimable (Publication 501, Dependent Taxpayer Test). + filer_is_dependent = person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) + return age_eligible & meets_identification_requirements & ~filer_is_dependent diff --git a/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py b/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py index 4caaab2f978..70f5b877edd 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py @@ -15,6 +15,7 @@ class ctc_adult_individual_maximum(Variable): "https://www.law.cornell.edu/uscode/text/26/24#a", "https://www.law.cornell.edu/uscode/text/26/24#h", "https://www.law.cornell.edu/uscode/text/26/24#i", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", ) def formula(person, period, parameters): @@ -24,9 +25,16 @@ def formula(person, period, parameters): filer_meets_tin_requirement = person.tax_unit( "filer_meets_ctc_identification_requirements", period ) + # IRC 24(h)(4) gives the credit for "any dependent of the taxpayer"; + # under IRC 152(b)(1) a return on which the filer (or, if joint, + # either spouse) can be claimed as a dependent has no dependents. + filer_is_dependent = person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) return ( is_adult * dependent_has_tin * filer_meets_tin_requirement + * ~filer_is_dependent * p.amount.adult_dependent ) diff --git a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py index 6509aa1f045..c3a557b0d8f 100644 --- a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py @@ -18,6 +18,11 @@ def formula(person, period, parameters): filing_status = tax_unit("filing_status", period) filing_status_values = filing_status.possible_values filing_status_eligible = filing_status != filing_status_values.SEPARATE + # IRC 25A(g)(3): no credit to an individual claimed as a dependent by + # another taxpayer, and the Form 1040 instructions extend this to + # "your spouse if filing jointly", so either filer being claimed bars + # the return's credit. + filer_claimed = tax_unit("head_or_spouse_is_dependent_elsewhere", period) requires_1098_t = aoc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( person("has_american_opportunity_credit_1098_t_or_exception", period) @@ -66,4 +71,5 @@ def formula(person, period, parameters): period, ) & filing_status_eligible + & ~filer_claimed ) diff --git a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py index 3d80f23da5f..75c2e133d15 100644 --- a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py @@ -19,6 +19,11 @@ def formula(person, period, parameters): filing_status = tax_unit("filing_status", period) filing_status_values = filing_status.possible_values filing_status_eligible = filing_status != filing_status_values.SEPARATE + # IRC 25A(g)(3): no credit to an individual claimed as a dependent by + # another taxpayer, and the Form 1040 instructions extend this to + # "your spouse if filing jointly", so either filer being claimed bars + # the return's credit. + filer_claimed = tax_unit("head_or_spouse_is_dependent_elsewhere", period) requires_1098_t = llc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( person("has_lifetime_learning_credit_1098_t_or_exception", period) @@ -45,4 +50,5 @@ def formula(person, period, parameters): period, ) & filing_status_eligible + & ~filer_claimed ) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py index 2250e5d6698..4e08716df78 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py @@ -13,6 +13,7 @@ class rrc_adult_count_with_valid_ssn(Variable): "https://www.law.cornell.edu/uscode/text/26/6428A#g_2", "https://www.law.cornell.edu/uscode/text/26/6428B#e_2_A", "https://www.law.cornell.edu/uscode/text/26/6428B#e_2_B", + "https://www.law.cornell.edu/uscode/text/26/6428#d_2", ) def formula(tax_unit, period, parameters): @@ -20,4 +21,9 @@ def formula(tax_unit, period, parameters): head_or_spouse = person("is_tax_unit_head_or_spouse", period) # Reuse existing EITC SSN check - identical definition (SSN required) has_valid_ssn = person("meets_eitc_identification_requirements", period) - return tax_unit.sum(head_or_spouse & has_valid_ssn) + # An "eligible individual" excludes "any individual with respect to + # whom a deduction under section 151 is allowable to another + # taxpayer" (IRC 6428(d)(2), 6428A(d)(2)) or "who is a dependent of + # another taxpayer" (IRC 6428B(c)(2)). + claimed = person("claimed_as_dependent_on_another_return", period) + return tax_unit.sum(head_or_spouse & has_valid_ssn & ~claimed) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py index 00b05333a86..e91e2ee9a48 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py @@ -13,6 +13,12 @@ class rrc_arpa_dependents_with_valid_ssn(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members - is_dependent = person("is_tax_unit_dependent", period) + # IRC 6428B(b)(2) counts "the number of dependents of the taxpayer", and + # under 152(b)(1) a return on which the filer (or, if joint, either + # spouse) can be claimed as a dependent has none. + dependent_filer = person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) + is_dependent = person("is_tax_unit_dependent", period) & ~dependent_filer has_valid_ssn = person("meets_eitc_identification_requirements", period) return tax_unit.sum(is_dependent & has_valid_ssn) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py index 9bf6a41feb0..3b45af22df5 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py @@ -19,9 +19,13 @@ class student_loan_interest_ald_eligible(Variable): ) def formula(person, period, parameters): - # Per IRC § 221(c), taxpayer cannot be claimed as a dependent head_or_spouse = person("is_tax_unit_head_or_spouse", period) + # Per IRC § 221(c), no deduction for an individual claimed as a + # dependent by another taxpayer; the Form 1040 instructions require + # that "you, or your spouse if filing jointly, aren't claimed as a + # dependent", so either filer being claimed bars the return. + filer_claimed = person.tax_unit("head_or_spouse_is_dependent_elsewhere", period) # Per IRC § 221(e)(2), taxpayer cannot file as Married Filing Separately filing_status = person.tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE - return head_or_spouse & ~separate + return head_or_spouse & ~separate & ~filer_claimed diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py b/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py index b8e19acbdab..2fec52064f4 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py @@ -7,5 +7,31 @@ class exemptions_count(Variable): label = "Number of tax exemptions" unit = USD definition_period = YEAR + documentation = ( + "Number of personal exemptions on the federal return: one for each " + "filer who cannot be claimed as a dependent by another taxpayer, plus " + "one for each dependent unless a filer can be claimed." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/151#d_2", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", + # Publication 501, Dependent Taxpayer Test. + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", + # 2017 Form 1040, line 6a: "If someone can claim you as a dependent, + # do not check box 6a"; instructions, line 6b: the spouse box only + # if the "spouse can't be claimed as a dependent". + "https://www.irs.gov/pub/irs-prior/f1040--2017.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/i1040gi--2017.pdf#page=15", + ) - adds = ["tax_unit_size"] + def formula(tax_unit, period, parameters): + # IRC 151(d)(2) zeroes the exemption of a filer whom another taxpayer + # can claim, and IRC 152(b)(1) treats a return on which the filer (or, + # if joint, either spouse) can be claimed as having no dependents. So + # such a return counts only its filers who cannot be claimed. + tax_unit_size = tax_unit("tax_unit_size", period) + filer_is_dependent = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + independent_filers = tax_unit( + "head_spouse_count_not_dependent_elsewhere", period + ) + return where(filer_is_dependent, independent_filers, tax_unit_size) 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 6722f22b3c2..9701ad23833 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 @@ -32,16 +32,27 @@ def formula(person, period, parameters): 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. - head_fixed = person.tax_unit.sum(is_head * fixed) + credit_per - spouse_fixed = person.tax_unit.sum(is_spouse * fixed) + credit_per + # 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)). + 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 two spouses. - dep_units = max_(total_units - 2, 0) + # 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 @@ -62,7 +73,7 @@ def formula(person, period, parameters): ) n_head_dep = where(eff_high > eff_low, n_high, n_low) - head_alloc = credit_per + n_head_dep * credit_per - spouse_alloc = credit_per + (dep_units - n_head_dep) * credit_per + 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) diff --git a/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py index 46ceaac454b..61195c8cb2c 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py @@ -16,7 +16,17 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ga.tax.income.credits.ctc person = tax_unit.members age = person("age", period) - ctc_eligible_child = person("ctc_qualifying_child", period) + # Georgia gives "qualifying child" the meaning of IRC 24(c) only, so + # it is not limited to children for whom the federal credit is + # allowed (IRC 24(a), 152(b)(1)). Whether a filer who can be claimed + # as a dependent can claim it is not settled; this keeps the IRC + # 24(c) child test. + p_federal = parameters(period).gov.irs.credits.ctc + ctc_eligible_child = ( + person("is_tax_unit_dependent", period) + & (age < p_federal.amount.base.thresholds[-1]) + & person("meets_ctc_child_identification_requirements", period) + ) ga_child_age_eligible = age < p.age_threshold eligible_children = tax_unit.sum(ctc_eligible_child & ga_child_age_eligible) diff --git a/policyengine_us/variables/gov/states/ga/tax/income/credits/ga_low_income_credit_potential.py b/policyengine_us/variables/gov/states/ga/tax/income/credits/ga_low_income_credit_potential.py index 4487e9e448a..07708fa354f 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/credits/ga_low_income_credit_potential.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/credits/ga_low_income_credit_potential.py @@ -15,7 +15,10 @@ def formula(tax_unit, period, parameters): # "multiplied by the number of dependents which the taxpayer is entitled to claim." # The tax form excludes adult dependents: # "Exemptions are self, spouse and natural or legally adopted children" - exemptions = tax_unit("exemptions_count", period) + # Georgia counts its own exemptions (O.C.G.A. 48-7A-3 deems each joint + # spouse a dependent), not the federal count, which leaves out a + # filer who can be claimed as a dependent. + exemptions = tax_unit("tax_unit_size", period) p = parameters(period).gov.states.ga.tax.income.credits.low_income # age threshold age_threshold = p.supplement_age_eligibility diff --git a/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.py b/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.py index 98a265dc9e8..c1540e1b65f 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_low_and_middle_income_exemption.py @@ -25,5 +25,8 @@ def formula(tax_unit, period, parameters): exemption_amount = p.max_amount - reduction_amount # Multiply the exemption amount by the number of exemptions + # NMSA 7-2-5.8 multiplies by the number of federal exemptions, which + # leaves out a filer who can be claimed as a dependent and, on such a + # return, the dependents. exemptions_count = tax_unit("exemptions_count", period) return eligible * exemption_amount * exemptions_count diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.py index 1fbb3a4d6b7..33bf6d2e2a3 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.py @@ -49,4 +49,10 @@ def formula(tax_unit, period, parameters): p.divisor, 1, ) - return rebate / divisor + # NMSA 7-2-14(A) allows the rebate only to a resident "who is not a + # dependent of another individual"; on a joint return the spouse who + # is not may still claim it (PIT-RC). The table starts at one + # exemption. + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) + eligible = ~every_filer_dependent & (exemptions > 0) + return where(eligible, rebate / divisor, 0) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.py b/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.py index cd8f89ad180..05621600269 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate_exemptions.py @@ -9,6 +9,7 @@ class nm_low_income_comprehensive_tax_rebate_exemptions(Variable): definition_period = YEAR reference = ( "https://law.justia.com/codes/new-mexico/chapter-7/article-2/section-7-2-14/", + "https://realfile.tax.newmexico.gov/2025pit-rc-ins.pdf#page=3", ) defined_for = StateCode.NM @@ -21,6 +22,11 @@ def formula(tax_unit, period, parameters): tax purposes for each individual included in the return who is domiciled in New Mexico... """ + # The federal count leaves out a filer who can be claimed as a + # dependent and, on such a return, the dependents, as PIT-RC line 2a + # does: "If either you or your spouse meet the general + # qualifications, but not both, then the non-qualifying spouse is not + # eligible". federal_exemptions = tax_unit("exemptions_count", period) """ ...plus two additional exemptions for each individual domiciled diff --git a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py index e222b7bf362..b78575932fb 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py @@ -18,6 +18,11 @@ def formula(person, period, parameters): dependent_on_another_return = person( "claimed_as_dependent_on_another_return", period ) - # The personal exemption is also provided to dependents - dependent = person("is_tax_unit_dependent", period) + # The personal exemption is also provided to dependents, but a return + # on which the filer (or, if joint, either spouse) can be claimed as a + # dependent has none (IRC 152(b)(1)). + dependent_filer = person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) + dependent = person("is_tax_unit_dependent", period) & ~dependent_filer return (~dependent_on_another_return & head_or_spouse) | dependent From bdd90309bfcbbf4ce0b4ac50ad78079be985a3be Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 12:36:16 -0400 Subject: [PATCH 08/32] Keep a filer who can be claimed elsewhere out of the premium tax credit family IRC 36B(c)(1)(D) excludes anyone another taxpayer can claim from being an applicable taxpayer, and 26 CFR 1.36B-1(d)(2) leaves such a spouse out of a joint return's family. Add is_aca_tax_family_member and aca_tax_family_size, measure household income against the poverty line for that family (aca_fpg, aca_magi_fraction), gate is_aca_ptc_eligible and the below-FPL immigration exception on it, and price the credit on the coverage family's benchmark (aca_ptc_slcsp, which equals slcsp whenever every enrollee is in the family). The NY/VT family-tier logic becomes shared functions so the benchmark can apply it to the coverage family. The three ACA reforms that override is_aca_ptc_eligible keep the gate. Co-Authored-By: Claude Opus 5.5 --- changelog.d/claimable-filer-ptc.fixed.md | 1 + .../reforms/aca/aca_ptc_700_fpl_cliff.py | 11 +- .../reforms/aca/aca_ptc_additional_bracket.py | 5 + .../reforms/aca/aca_ptc_simplified_bracket.py | 5 + .../baseline/gov/aca/eligibility/aca_fpg.yaml | 238 +++++++++ .../aca/eligibility/aca_magi_fraction.yaml | 123 +++++ ...a_ptc_below_fpl_immigration_exception.yaml | 105 ++++ .../aca/eligibility/aca_tax_family_size.yaml | 233 +++++++++ .../is_aca_coverage_family_member.yaml | 115 +++++ .../aca/eligibility/is_aca_ptc_eligible.yaml | 344 +++++++++++++ .../eligibility/is_aca_tax_family_member.yaml | 209 ++++++++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 470 ++++++++++++++++++ .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 372 ++++++++++++++ .../aca_ptc_slcsp_family_tier_category.yaml | 285 +++++++++++ .../aca_ptc_slcsp_family_tier_multiplier.yaml | 215 ++++++++ .../policy/contrib/aca/ptc_700_fpl_cliff.yaml | 73 +++ .../contrib/aca/ptc_additional_bracket.yaml | 73 +++ .../contrib/aca/ptc_simplified_bracket.yaml | 73 +++ .../variables/gov/aca/eligibility/aca_fpg.py | 31 ++ .../gov/aca/eligibility/aca_magi_fraction.py | 19 +- ...aca_ptc_below_fpl_immigration_exception.py | 8 +- .../aca/eligibility/aca_tax_family_size.py | 21 + .../is_aca_coverage_family_member.py | 25 + .../aca/eligibility/is_aca_ptc_eligible.py | 16 +- .../eligibility/is_aca_tax_family_member.py | 37 ++ .../variables/gov/aca/ptc/aca_ptc.py | 4 +- .../variables/gov/aca/slspc/aca_ptc_slcsp.py | 42 ++ .../aca_ptc_slcsp_family_tier_category.py | 26 + .../aca_ptc_slcsp_family_tier_multiplier.py | 21 + .../aca/slspc/slcsp_family_tier_category.py | 154 +++--- .../aca/slspc/slcsp_family_tier_multiplier.py | 106 ++-- 31 files changed, 3317 insertions(+), 143 deletions(-) create mode 100644 changelog.d/claimable-filer-ptc.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml create mode 100644 policyengine_us/variables/gov/aca/eligibility/aca_fpg.py create mode 100644 policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py create mode 100644 policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py create mode 100644 policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py create mode 100644 policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py diff --git a/changelog.d/claimable-filer-ptc.fixed.md b/changelog.d/claimable-filer-ptc.fixed.md new file mode 100644 index 00000000000..00c137474dd --- /dev/null +++ b/changelog.d/claimable-filer-ptc.fixed.md @@ -0,0 +1 @@ +Apply IRC 36B(c)(1)(D) to the premium tax credit: a filer who can be claimed as a dependent on another return is outside the tax family (26 CFR 1.36B-1(d)(2)), so the credit uses the tax family size for the poverty line and the coverage family's benchmark premium, on a joint return the other spouse keeps the credit, and the Washington, Massachusetts, Connecticut, Colorado, Maryland, California, New Mexico, Vermont and New Jersey premium programs follow their own rules for that person. diff --git a/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py b/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py index c9d449f6981..38002e777ac 100644 --- a/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py +++ b/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py @@ -66,7 +66,16 @@ def formula(person, period, parameters): ) is_income_eligible = standard_income_eligible | below_fpl_exception - return person("pays_aca_premium", period) & ~separate & is_income_eligible + # The reform leaves 26 U.S.C. 36B(c)(1)(D) in place: someone + # another taxpayer can claim is outside the tax family. + tax_family_member = person("is_aca_tax_family_member", period) + + return ( + person("pays_aca_premium", period) + & tax_family_member + & ~separate + & is_income_eligible + ) class reform(Reform): def apply(self): diff --git a/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py b/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py index a7ce0497a2d..b98cda69863 100644 --- a/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py +++ b/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py @@ -73,11 +73,16 @@ def formula(person, period, parameters): child_pays = person("aca_child_index", period) <= p_aca.max_child_count pays_aca_premium = is_aca_adult | child_pays + # The reform leaves 26 U.S.C. 36B(c)(1)(D) in place: someone + # another taxpayer can claim is outside the tax family. + tax_family_member = person("is_aca_tax_family_member", period) + return ( is_status_eligible & is_coverage_eligible & is_income_eligible & pays_aca_premium + & tax_family_member ) class reform(Reform): diff --git a/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py b/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py index 38603128235..9968a6a8728 100644 --- a/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py +++ b/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py @@ -74,11 +74,16 @@ def formula(person, period, parameters): child_pays = person("aca_child_index", period) <= p_aca.max_child_count pays_aca_premium = is_aca_adult | child_pays + # The reform leaves 26 U.S.C. 36B(c)(1)(D) in place: someone + # another taxpayer can claim is outside the tax family. + tax_family_member = person("is_aca_tax_family_member", period) + return ( is_status_eligible & is_coverage_eligible & is_income_eligible & pays_aca_premium + & tax_family_member ) class reform(Reform): diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml new file mode 100644 index 00000000000..9d31224ee0b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml @@ -0,0 +1,238 @@ +- name: 2025 joint return with one claimable spouse and a child uses the one-person poverty line (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 2024 contiguous guideline for one person (parameters/gov/hhs/fpg.yaml). + aca_tax_family_size: 1 + aca_fpg: 15_060 + +- name: 2025 joint return with one claimable spouse and a child uses the one-person poverty line (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_tax_family_size: 1 + aca_fpg: 15_060 + +- name: 2025 joint return with neither spouse claimable and a child uses the three-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 15,060 + 2 * 5,380 + aca_fpg: 25_820 + +- name: 2025 single filer who can be claimed keeps the tax unit's one-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + households: + household: + members: [person_a] + state_code: TX + output: + # No tax family, so the line for the whole tax unit (1 person). + aca_tax_family_size: 0 + aca_fpg: 15_060 + +- name: 2025 joint return with both spouses claimable keeps the tax unit's two-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_tax_family_size: 0 + aca_fpg: 20_440 + +- name: 2026 joint return with one claimable spouse uses the 2025 one-person line (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_fpg: 15_650 + +- name: 2026 joint return with one claimable spouse uses the 2025 one-person line (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_fpg: 15_650 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml new file mode 100644 index 00000000000..e352b8a0826 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml @@ -0,0 +1,123 @@ +- name: 2025 joint return with one claimable spouse measures income against the one-person line (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 22,590 / 15,060 = 1.50 + aca_magi_fraction: 1.5 + +- name: 2025 joint return with one claimable spouse measures income against the one-person line (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_magi_fraction: 1.5 + +- name: 2025 joint return with neither spouse claimable measures income against the two-person line + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 22,590 / 20,440 = 1.105, truncated to 1.10 (Form 8962 Worksheet 2) + aca_magi_fraction: 1.1 + +- name: 2025 single filer who can be claimed keeps the one-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + households: + household: + members: [person_a] + state_code: TX + output: + aca_magi_fraction: 1.5 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml new file mode 100644 index 00000000000..fd777a8eb45 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml @@ -0,0 +1,105 @@ +- name: 2025 below-FPL exception does not apply when the lawfully present spouse can be claimed elsewhere (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + immigration_status: TPS + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi_fraction: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 26 CFR 1.36B-2(b)(5): the taxpayer or a member of the taxpayer's + # family must be the lawfully present person. + aca_ptc_below_fpl_immigration_exception: false + +- name: 2025 below-FPL exception does not apply when the lawfully present spouse can be claimed elsewhere (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + immigration_status: TPS + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi_fraction: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc_below_fpl_immigration_exception: false + +- name: 2025 below-FPL exception applies when the lawfully present spouse is in the tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + immigration_status: TPS + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi_fraction: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_below_fpl_immigration_exception: true diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml new file mode 100644 index 00000000000..636b3ff9473 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml @@ -0,0 +1,233 @@ +- name: 2025 single filer who can be claimed elsewhere has tax family size 0 + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + # Form 8962 line 1 (2025 instructions p.8): if someone can claim you, or + # your spouse on a joint return, you or your spouse is not included in + # the tax family size. + aca_tax_family_size: 0 + tax_unit_size: 2 + +- name: 2025 joint return with one claimable spouse and two children has tax family size 1 (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child, child2] + state_code: TX + output: + # Form 8962 line 1 (2025 instructions p.8): if someone can claim you, or + # your spouse on a joint return, you or your spouse is not included in + # the tax family size. + aca_tax_family_size: 1 + tax_unit_size: 4 + +- name: 2025 joint return with one claimable spouse and two children has tax family size 1 (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child, child2] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child, child2] + state_code: TX + output: + aca_tax_family_size: 1 + tax_unit_size: 4 + +- name: 2025 joint return with both spouses claimable has tax family size 0 + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + aca_tax_family_size: 0 + +- name: 2025 joint return with neither spouse claimable counts the whole tax unit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child, child2] + state_code: TX + output: + aca_tax_family_size: 4 + +- name: 2025 single parent who cannot be claimed counts their child + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + aca_tax_family_size: 2 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml new file mode 100644 index 00000000000..6064d5ac59d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml @@ -0,0 +1,115 @@ +- name: "2025 joint return with one claimable spouse: the coverage family is the other spouse (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-3(b)(2): members of the taxpayer's family who enroll. + pays_aca_premium: [true, true, true] + is_aca_coverage_family_member: [false, true, false] + +- name: "2025 joint return with one claimable spouse: the coverage family is the other spouse (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + pays_aca_premium: [true, true, true] + is_aca_coverage_family_member: [true, false, false] + +- name: "2025 joint return with neither spouse claimable: a Medicaid-eligible child is not in the coverage family" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + is_aca_coverage_family_member: [true, true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml index 0c8789a0467..7d03af20362 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml @@ -495,3 +495,347 @@ has_qualifying_non_marketplace_health_coverage_at_interview: [true] pays_aca_premium: [false] is_aca_ptc_eligible: [false] + +- name: "2025 joint return with one claimable spouse: only the other spouse is eligible (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # IRC 36B(c)(1)(D); 26 CFR 1.36B-2(b)(3): someone another taxpayer may + # claim is not an applicable taxpayer. + aca_magi_fraction: 1.5 + is_aca_ptc_eligible: [false, true] + +- name: "2025 joint return with one claimable spouse: only the other spouse is eligible (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_magi_fraction: 1.5 + is_aca_ptc_eligible: [true, false] + +- name: 2025 single filer who can be claimed elsewhere is not eligible + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + households: + household: + members: [person_a] + state_code: TX + output: + is_aca_ptc_eligible: [false] + +- name: "2025 joint return with both spouses claimable: neither is eligible" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + is_aca_ptc_eligible: [false, false] + +- name: "2025 joint return with neither spouse claimable: both are eligible" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + is_aca_ptc_eligible: [true, true] + +- name: "2025 joint return with one claimable spouse: their child is not eligible either (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # IRC 152(b)(1): the return claims no dependents. + is_aca_ptc_eligible: [false, true, false] + +- name: "2025 joint return with one claimable spouse: their child is not eligible either (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + is_aca_ptc_eligible: [true, false, false] + +- name: "2025 couple below the two-person line but above the one-person line: the unclaimable spouse is eligible (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 18,000 / 15,060 = 1.195 for a tax family of one, so 100-400% FPL; + # the same couple with nobody claimable is at 0.88 and not eligible. + aca_magi_fraction: 1.19 + is_aca_ptc_eligible: [false, true] + +- name: "2025 couple below the two-person line but above the one-person line: the unclaimable spouse is eligible (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 18_000 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_magi_fraction: 1.19 + is_aca_ptc_eligible: [true, false] + +- name: 2025 couple below the two-person line with neither spouse claimable is not eligible + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_magi_fraction: 0.88 + is_aca_ptc_eligible: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml new file mode 100644 index 00000000000..7678861083c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -0,0 +1,209 @@ +- name: 2025 single filer who can be claimed elsewhere, and their child, are outside the tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2): the family is the taxpayer, both spouses on a + # joint return, except for individuals who qualify as a dependent of + # another taxpayer; IRC 152(b)(1): such a return claims no dependents. + is_aca_tax_family_member: [false, false] + +- name: 2025 joint return with one claimable spouse keeps only the other spouse in the tax family (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2): the family is the taxpayer, both spouses on a + # joint return, except for individuals who qualify as a dependent of + # another taxpayer; IRC 152(b)(1): such a return claims no dependents. + is_aca_tax_family_member: [false, true, false] + +- name: 2025 joint return with one claimable spouse keeps only the other spouse in the tax family (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + is_aca_tax_family_member: [true, false, false] + +- name: 2025 joint return with both spouses claimable has no tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + is_aca_tax_family_member: [false, false, false] + +- name: 2025 joint return with neither spouse claimable keeps everyone in the tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + is_aca_tax_family_member: [true, true, true] + +- name: 2025 dependent flagged as claimed elsewhere stays in the family of the return that claims them + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child: + age: 10 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + is_aca_tax_family_member: [true, true] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index e16d13d56e5..49a6222cef5 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -256,3 +256,473 @@ output: is_aca_ptc_eligible: [false] aca_ptc: 0 + +- name: "2025 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 150% FPL for one, 0% contribution: 6,000 - 0 + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 6000 + +- name: "2025 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 6000 + +- name: 2025 single filer who can be claimed elsewhere gets no credit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a] + state_code: TX + output: + # Form 8962 instructions: no one can claim you as a dependent. + aca_ptc: 0 + +- name: 2025 joint return with both spouses claimable gets no credit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc: 0 + +- name: "2025 joint return with neither spouse claimable: credit for both benchmarks" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 2025: 110% FPL for two, 0% contribution. + aca_tax_family_size: 2 + aca_ptc: 12_000 + +- name: "2026 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the head)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 150% FPL for one, 4.19% contribution: 6,000 - 0.0419 * 23,475 + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 5016.4 + +- name: "2026 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the spouse)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 5016.4 + +- name: 2026 single filer who can be claimed elsewhere gets no credit + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a] + state_code: TX + output: + # Form 8962 instructions: no one can claim you as a dependent. + aca_ptc: 0 + +- name: 2026 joint return with both spouses claimable gets no credit + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc: 0 + +- name: "2026 joint return with neither spouse claimable: credit for both benchmarks" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 2026: 23,475 / 21,150 = 110%, 2.1%: 12,000 - 0.021 * 23,475 + aca_tax_family_size: 2 + aca_ptc: 11507.03 + +- name: "2025 couple below the two-person line: the unclaimable spouse gets a credit (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 18,000 / 15,060 = 119% FPL for a family of one: 0% contribution. + aca_ptc: 6000 + +- name: "2025 couple below the two-person line: the unclaimable spouse gets a credit (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 18_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc: 6000 + +- name: 2025 couple below the two-person line with neither spouse claimable gets no credit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml new file mode 100644 index 00000000000..5720af34beb --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -0,0 +1,372 @@ +- name: "2025 joint return with one claimable spouse: the benchmark covers only the other spouse (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-3(f): the benchmark offered to the coverage family; + # slcsp still counts every enrollee. + slcsp: 15_000 + aca_ptc_slcsp: 6000 + +- name: "2025 joint return with one claimable spouse: the benchmark covers only the other spouse (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + slcsp: 15_000 + aca_ptc_slcsp: 6000 + +- name: "2025 joint return with neither spouse claimable: the benchmark is slcsp" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + slcsp: 15_000 + aca_ptc_slcsp: 15_000 + +- name: "2025 single filer who can be claimed: no coverage family, no benchmark" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + households: + household: + members: [person_a] + state_code: TX + output: + slcsp: 6000 + aca_ptc_slcsp: 0 + +- name: "2025 joint return with both spouses claimable: no benchmark" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + slcsp: 12_000 + aca_ptc_slcsp: 0 + +- name: 2025 a supplied slcsp carries through when every enrollee is in the coverage family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + slcsp: 1200 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_slcsp: 1200 + +- name: "2025 NY joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the head)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp: 2166.0 + aca_ptc_slcsp: 760 + +- name: "2025 NY joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the spouse)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp: 2166.0 + aca_ptc_slcsp: 760 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the head)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp: 2135.6 + aca_ptc_slcsp: 760 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the spouse)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp: 2135.6 + aca_ptc_slcsp: 760 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml new file mode 100644 index 00000000000..80cf923afc6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml @@ -0,0 +1,285 @@ +- name: "2025 NY joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 NY joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 NY joint return with both spouses claimable: no coverage family tier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_category: INDIVIDUAL_AGE_RATED + +- name: "2025 NY joint return with neither spouse claimable: same tier as slcsp" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + +- name: "2025 VT joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 VT joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 VT joint return with both spouses claimable: no coverage family tier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_category: INDIVIDUAL_AGE_RATED + +- name: "2025 VT joint return with neither spouse claimable: same tier as slcsp" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml new file mode 100644 index 00000000000..e175f8f9941 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml @@ -0,0 +1,215 @@ +- name: "2025 NY joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.85 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 NY joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.85 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 NY joint return with both spouses claimable: zero multiplier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_multiplier: 0 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.81 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.81 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 VT joint return with both spouses claimable: zero multiplier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_multiplier: 0 diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml index ba79805f0ff..737cdc45428 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml @@ -298,3 +298,76 @@ output: aca_ptc_below_fpl_immigration_exception: true is_aca_ptc_eligible: [true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the head) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_700_fpl_cliff.aca_ptc_700_fpl_cliff + input: + gov.contrib.aca.ptc_700_fpl_cliff.in_effect: true + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 46,950 / 15,650 = 300% FPL for a family of one. + aca_tax_family_size: 1 + is_aca_ptc_eligible: [false, true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the spouse) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_700_fpl_cliff.aca_ptc_700_fpl_cliff + input: + gov.contrib.aca.ptc_700_fpl_cliff.in_effect: true + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + is_aca_ptc_eligible: [true, false] diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml index 3f5846288f5..ce8da297e6e 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml @@ -115,3 +115,76 @@ aca_magi_fraction: 1.75 output: aca_required_contribution_percentage: 0.01 + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the head) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_additional_bracket.aca_ptc_additional_bracket + input: + gov.contrib.aca.ptc_additional_bracket.in_effect: true + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 46,950 / 15,650 = 300% FPL for a family of one. + aca_tax_family_size: 1 + is_aca_ptc_eligible: [false, true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the spouse) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_additional_bracket.aca_ptc_additional_bracket + input: + gov.contrib.aca.ptc_additional_bracket.in_effect: true + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + is_aca_ptc_eligible: [true, false] diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml index 93bb7d35bdd..820a2cb1b42 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml @@ -115,3 +115,76 @@ aca_magi_fraction: 6.0 output: aca_required_contribution_percentage: 0.20 + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the head) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_simplified_bracket.aca_ptc_simplified_bracket + input: + gov.contrib.aca.ptc_simplified_bracket.in_effect: true + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 46,950 / 15,650 = 300% FPL for a family of one. + aca_tax_family_size: 1 + is_aca_ptc_eligible: [false, true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the spouse) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_simplified_bracket.aca_ptc_simplified_bracket + input: + gov.contrib.aca.ptc_simplified_bracket.in_effect: true + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + is_aca_ptc_eligible: [true, false] diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py new file mode 100644 index 00000000000..2a882f1a066 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg + + +class aca_fpg(Variable): + value_type = float + entity = TaxUnit + label = "Federal poverty line for the premium tax credit tax family" + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#d_3", + # Line 1 (tax family size) and line 4 (poverty line). + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + ) + documentation = ( + "Form 8962 line 4: the prior year's poverty guideline for the tax " + "family size on line 1 (aca_tax_family_size). A return on which " + "every head and spouse can be claimed elsewhere has no tax family and " + "no applicable taxpayer; it keeps the guideline for the whole tax " + "unit, so readers that are not about the credit see the same value " + "as before." + ) + + def formula(tax_unit, period, parameters): + family_size = tax_unit("aca_tax_family_size", period) + size = where(family_size > 0, family_size, tax_unit("tax_unit_size", period)) + # The guideline year lags the tax year; the state group is read for + # the prior year, as the guideline lookup did before. + state_group = tax_unit.household("state_group_str", period.last_year) + return fpg(size, state_group, period, parameters, year_lag=1) diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py index 9464de7bdd2..d3780926dfe 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py @@ -6,23 +6,18 @@ class aca_magi_fraction(Variable): entity = TaxUnit label = "ACA-related modified AGI as fraction of prior-year FPL" documentation = ( - "ACA-related MAGI as fraction of federal poverty line. The first " - "reference documents the use of the prior-year FPL; the second " - "documents the truncation of the fraction." + "Household income as a fraction of the prior-year federal poverty " + "line for the premium tax credit tax family (Form 8962 line 5), " + "truncated to whole percentage points as Worksheet 2 instructs." ) reference = ( - dict( - title="2022 IRS Form 8962 (ACA PTC) instructions, Line 4", - href="https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=7", - ), - dict( - title="2022 IRS Form 8962 instructions, Line 5 Worksheet 2", - href="https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=8", - ), + # PDF pages 8-9: line 4 (prior-year poverty line for the line 1 + # family size) and line 5 Worksheet 2 (truncation). + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", ) definition_period = YEAR def formula(tax_unit, period, parameters): magi = max_(0, tax_unit("aca_magi", period)) - fpg = tax_unit("tax_unit_fpg", period.last_year) + fpg = tax_unit("aca_fpg", period) return np.floor(100 * magi / fpg) / 100 diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py b/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py index d12ea8c17cf..207731b8c21 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py @@ -23,7 +23,13 @@ def formula(tax_unit, period, parameters): "is_medicaid_immigration_status_eligible", period ) + # 26 CFR 1.36B-2(b)(5): "the taxpayer or a member of the taxpayer's + # family" must be the lawfully present, Medicaid-ineligible person. + tax_family_member = person("is_aca_tax_family_member", period) qualifying_family_member = tax_unit.any( - non_citizen & aca_lawfully_present & medicaid_ineligible_due_to_status + tax_family_member + & non_citizen + & aca_lawfully_present + & medicaid_ineligible_due_to_status ) return in_effect & below_fpl & qualifying_family_member diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py new file mode 100644 index 00000000000..d8cea21ac2d --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class aca_tax_family_size(Variable): + value_type = int + entity = TaxUnit + label = "Premium tax credit tax family size" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#d_1", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d", + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + ) + documentation = ( + "Form 8962 line 1: the number of people in the premium tax credit " + "tax family. It equals the tax unit size unless a head or spouse can " + "be claimed as a dependent on another return, and is 0 when every " + "head and spouse can be. The shared tax_unit_size is unchanged." + ) + + adds = ["is_aca_tax_family_member"] diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py new file mode 100644 index 00000000000..022482995d0 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class is_aca_coverage_family_member(Variable): + value_type = bool + entity = Person + label = "Member of the premium tax credit coverage family" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2", + "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f_1", + ) + documentation = ( + "A Marketplace enrollee (pays_aca_premium) who is in the premium tax " + "credit tax family. 26 CFR 1.36B-3(b)(2) defines the coverage family " + "as the members of the taxpayer's family for whom the month is a " + "coverage month, and the benchmark plan in 1.36B-3(f) is the one " + "offered to the coverage family. An enrollee who can be claimed on " + "another return buys a plan but is not in it." + ) + + def formula(person, period, parameters): + return person("pays_aca_premium", period) & person( + "is_aca_tax_family_member", period + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py index b21baa83d3c..34e4cc60e67 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py @@ -6,6 +6,10 @@ class is_aca_ptc_eligible(Variable): entity = Person label = "Person is eligible for ACA premium tax credit and pays ACA premium" definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#c_1", + "https://www.law.cornell.edu/cfr/text/26/1.36B-2#b", + ) def formula(person, period, parameters): fstatus = person.tax_unit("filing_status", period) @@ -20,4 +24,14 @@ def formula(person, period, parameters): ) is_income_eligible = standard_income_eligible | below_fpl_exception - return person("pays_aca_premium", period) & ~separate & is_income_eligible + # Someone another taxpayer can claim is not an applicable taxpayer + # (26 U.S.C. 36B(c)(1)(D)) and is outside the tax family, along with + # the dependents of a return that has such a filer. + tax_family_member = person("is_aca_tax_family_member", period) + + return ( + person("pays_aca_premium", period) + & tax_family_member + & ~separate + & is_income_eligible + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py new file mode 100644 index 00000000000..ea43bed46d3 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -0,0 +1,37 @@ +from policyengine_us.model_api import * + + +class is_aca_tax_family_member(Variable): + value_type = bool + entity = Person + label = "Member of the premium tax credit tax family" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#c_1_D", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d_2", + "https://www.law.cornell.edu/cfr/text/26/1.36B-2#b_3", + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + ) + documentation = ( + "Whether the person is in the taxpayer's family for the premium tax " + "credit (Form 8962 line 1, tax family size). 26 CFR 1.36B-1(d)(2) " + "defines the family as the taxpayer, both spouses on a joint return, " + "'except for individuals who qualify as a dependent of another " + "taxpayer', plus the dependents the taxpayer claims. A head or spouse " + "who can be claimed on another return is therefore not a member " + "(IRC 36B(c)(1)(D), 26 CFR 1.36B-2(b)(3)), and on a joint return the " + "other spouse still is. A return with a filer who can be claimed " + "elsewhere claims no dependents (IRC 152(b)(1)), so its dependents " + "are members only when no head or spouse can be claimed elsewhere. " + "The claimed flag of a dependent on this return does not matter: this " + "return claims them." + ) + + def formula(person, period, parameters): + filer = person("is_tax_unit_head_or_spouse", period) + claimed_elsewhere = person("claimed_as_dependent_on_another_return", period) + return_has_claimed_filer = person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) + return where(filer, ~claimed_elsewhere, ~return_has_claimed_filer) diff --git a/policyengine_us/variables/gov/aca/ptc/aca_ptc.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc.py index 76c9fbe2464..79a7ff5a45a 100644 --- a/policyengine_us/variables/gov/aca/ptc/aca_ptc.py +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc.py @@ -19,7 +19,9 @@ def formula(tax_unit, period, parameters): return 0 def formula_2018(tax_unit, period, parameters): - plan_cost = tax_unit("slcsp", period) + # The benchmark for the coverage family (26 CFR 1.36B-3(f)), which + # leaves out an enrollee who can be claimed on another return. + plan_cost = tax_unit("aca_ptc_slcsp", period) income = tax_unit("aca_magi", period) applicable_figure = tax_unit("aca_required_contribution_percentage", period) # IRC § 36B(a) conditions the PTC on filing a return (Form 8962). diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py new file mode 100644 index 00000000000..ea2eaf3ae9a --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class aca_ptc_slcsp(Variable): + value_type = float + entity = TaxUnit + label = "Benchmark silver-plan premium for the premium tax credit" + unit = USD + definition_period = MONTH + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#b_3_B", + "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f", + ) + documentation = ( + "The second-lowest-cost silver plan premium offered to the coverage " + "family (26 CFR 1.36B-3(f)): the enrollees who are in the premium tax " + "credit tax family. When every enrollee on the return is in it, this " + "is slcsp itself, read directly so a supplied slcsp carries through. " + "When an enrollee is not (one who can be claimed on another return, " + "or a dependent of a return with such a filer), it is the age-curve " + "premiums of the coverage family members or, where family tiers " + "apply, the tier premium for them. slcsp keeps every enrollee, since " + "everyone still buys a plan." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + enrolled = person("pays_aca_premium", period.this_year) + covered = person("is_aca_coverage_family_member", period.this_year) + every_enrollee_covered = ~tax_unit.any(enrolled & ~covered) + age_curve = tax_unit.sum( + covered * person("slcsp_age_curve_amount_person", period) + ) + family_tier = tax_unit.household("slcsp_age_0", period) * tax_unit( + "aca_ptc_slcsp_family_tier_multiplier", period + ) + coverage_family_benchmark = age_curve + family_tier + return where( + every_enrollee_covered, + tax_unit("slcsp", period), + coverage_family_benchmark, + ) diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py new file mode 100644 index 00000000000..cd6475506cb --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( + FamilyTierCategory, + family_tier_category, +) + + +class aca_ptc_slcsp_family_tier_category(Variable): + value_type = Enum + entity = TaxUnit + possible_values = FamilyTierCategory + default_value = FamilyTierCategory.INDIVIDUAL_AGE_RATED + definition_period = MONTH + label = "ACA family tier category for the premium tax credit coverage family" + defined_for = "slcsp_family_tier_applies" + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f" + documentation = ( + "The family tier (New York and Vermont) of the coverage family's " + "benchmark plan: slcsp_family_tier_category counted over coverage " + "family members only, so an enrollee who can be claimed on another " + "return does not move the tier." + ) + + def formula(tax_unit, period, parameters): + covered = tax_unit.members("is_aca_coverage_family_member", period.this_year) + return family_tier_category(tax_unit, period, parameters, covered) diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py new file mode 100644 index 00000000000..4edacc5d1a6 --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_multiplier import ( + family_tier_multiplier, +) + + +class aca_ptc_slcsp_family_tier_multiplier(Variable): + value_type = float + entity = TaxUnit + label = "ACA family tier multiplier for the premium tax credit coverage family" + unit = "/1" + definition_period = MONTH + defined_for = "slcsp_family_tier_applies" + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f" + + def formula(tax_unit, period, parameters): + family_category = tax_unit("aca_ptc_slcsp_family_tier_category", period) + covered = tax_unit.members("is_aca_coverage_family_member", period.this_year) + return family_tier_multiplier( + tax_unit, period, parameters, family_category, covered + ) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py index 147aa042035..1b9a354fdd4 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py @@ -10,6 +10,80 @@ class FamilyTierCategory(Enum): CHILD_ONLY = "Child_only" +def family_tier_category(tax_unit, period, parameters, enrolled): + """ + Determine the family tier category for premium calculation in states + that use family tiers instead of individual age rating, counting only + the members flagged in `enrolled` (a person-level boolean array). + + Categories for both NY and VT: + - One_adult + - Two_adults + - One_adult_and_one_or_more_children + - Two_adults_and_one_or_more_children + - Child_only (only for NY) + """ + # Get inputs + state_code = tax_unit.household("state_code_str", period) + + person = tax_unit.members + dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + non_dependent_adult_count = tax_unit.sum(enrolled & ~dependent_child) + anchored_child_count = tax_unit.sum(dependent_child) + + under_child_only_age = ( + person("monthly_age", period) <= parameters(period).gov.aca.slcsp.max_child_age + ) + child_only_child = enrolled & under_child_only_age + unanchored_adult_count = tax_unit.sum(enrolled & ~child_only_child) + unanchored_child_count = tax_unit.sum(child_only_child) + has_adult_anchor = non_dependent_adult_count > 0 + eligible_adult_count = where( + has_adult_anchor, + non_dependent_adult_count, + unanchored_adult_count, + ) + eligible_child_count = where( + has_adult_anchor, + anchored_child_count, + unanchored_child_count, + ) + + # Common conditions for both states + one_adult_no_children = (eligible_adult_count == 1) & (eligible_child_count == 0) + two_plus_adults_no_children = (eligible_adult_count >= 2) & ( + eligible_child_count == 0 + ) + one_adult_with_children = (eligible_adult_count == 1) & (eligible_child_count > 0) + two_plus_adults_with_children = (eligible_adult_count >= 2) & ( + eligible_child_count > 0 + ) + + # NY-specific condition (child-only households) + ny_child_only = ( + (state_code == "NY") & (eligible_adult_count == 0) & (eligible_child_count > 0) + ) + + return select( + [ + # Apply conditions only to family tier states + ny_child_only, + one_adult_no_children, + two_plus_adults_no_children, + one_adult_with_children, + two_plus_adults_with_children, + ], + [ + FamilyTierCategory.CHILD_ONLY, + FamilyTierCategory.ONE_ADULT, + FamilyTierCategory.TWO_ADULTS, + FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + ], + default=FamilyTierCategory.INDIVIDUAL_AGE_RATED, + ) + + class slcsp_family_tier_category(Variable): value_type = Enum entity = TaxUnit @@ -20,81 +94,5 @@ class slcsp_family_tier_category(Variable): defined_for = "slcsp_family_tier_applies" def formula(tax_unit, period, parameters): - """ - Determine the family tier category for premium calculation in states - that use family tiers instead of individual age rating. - - Categories for both NY and VT: - - One_adult - - Two_adults - - One_adult_and_one_or_more_children - - Two_adults_and_one_or_more_children - - Child_only (only for NY) - """ - # Get inputs - state_code = tax_unit.household("state_code_str", period) - - person = tax_unit.members - pays_premium = person("pays_aca_premium", period) - dependent_child = person("is_aca_family_tier_dependent_child", period) - non_dependent_adult_count = tax_unit.sum(pays_premium & ~dependent_child) - anchored_child_count = tax_unit.sum(dependent_child) - - under_child_only_age = ( - person("monthly_age", period) - <= parameters(period).gov.aca.slcsp.max_child_age - ) - child_only_child = pays_premium & under_child_only_age - unanchored_adult_count = tax_unit.sum(pays_premium & ~child_only_child) - unanchored_child_count = tax_unit.sum(child_only_child) - has_adult_anchor = non_dependent_adult_count > 0 - eligible_adult_count = where( - has_adult_anchor, - non_dependent_adult_count, - unanchored_adult_count, - ) - eligible_child_count = where( - has_adult_anchor, - anchored_child_count, - unanchored_child_count, - ) - - # Common conditions for both states - one_adult_no_children = (eligible_adult_count == 1) & ( - eligible_child_count == 0 - ) - two_plus_adults_no_children = (eligible_adult_count >= 2) & ( - eligible_child_count == 0 - ) - one_adult_with_children = (eligible_adult_count == 1) & ( - eligible_child_count > 0 - ) - two_plus_adults_with_children = (eligible_adult_count >= 2) & ( - eligible_child_count > 0 - ) - - # NY-specific condition (child-only households) - ny_child_only = ( - (state_code == "NY") - & (eligible_adult_count == 0) - & (eligible_child_count > 0) - ) - - return select( - [ - # Apply conditions only to family tier states - ny_child_only, - one_adult_no_children, - two_plus_adults_no_children, - one_adult_with_children, - two_plus_adults_with_children, - ], - [ - FamilyTierCategory.CHILD_ONLY, - FamilyTierCategory.ONE_ADULT, - FamilyTierCategory.TWO_ADULTS, - FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - ], - default=FamilyTierCategory.INDIVIDUAL_AGE_RATED, - ) + enrolled = tax_unit.members("pays_aca_premium", period) + return family_tier_category(tax_unit, period, parameters, enrolled) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py index 64f85feb3d7..194ac280ae0 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py @@ -4,6 +4,60 @@ ) +def family_tier_multiplier(tax_unit, period, parameters, family_category, enrolled): + """ + Family tier premium multiplier for `family_category`, counting only the + members flagged in `enrolled` (a person-level boolean array). + """ + p = parameters(period).gov.aca.family_tier_ratings + state_code = tax_unit.household("state_code", period) + in_ny = state_code == state_code.possible_values.NY + + one_adult = where(in_ny, p.ny.ONE_ADULT, p.vt.ONE_ADULT) + two_adults = where(in_ny, p.ny.TWO_ADULTS, p.vt.TWO_ADULTS) + one_adult_with_children = where( + in_ny, + p.ny.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + p.vt.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + ) + two_adults_with_children = where( + in_ny, + p.ny.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + p.vt.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + ) + child_only = where(in_ny, p.ny.CHILD_ONLY, 0) + + base_multiplier = select( + [ + family_category == FamilyTierCategory.ONE_ADULT, + family_category == FamilyTierCategory.TWO_ADULTS, + family_category == FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + family_category == FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + family_category == FamilyTierCategory.CHILD_ONLY, + ], + [ + one_adult, + two_adults, + one_adult_with_children, + two_adults_with_children, + child_only, + ], + default=0, + ) + + person = tax_unit.members + dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + adult_count = tax_unit.sum(enrolled & ~dependent_child) + extra_adults = max_(adult_count - 2, 0) + age_29_child = enrolled & person("is_aca_ny_age_29_dependent_child", period) + age_29_multiplier = where( + tax_unit.any(age_29_child), + parameters(period).gov.aca.ny_age_29_dependent_child_tier_multiplier, + 1, + ) + return base_multiplier * age_29_multiplier + extra_adults * one_adult + + class slcsp_family_tier_multiplier(Variable): value_type = float entity = TaxUnit @@ -14,53 +68,7 @@ class slcsp_family_tier_multiplier(Variable): def formula(tax_unit, period, parameters): family_category = tax_unit("slcsp_family_tier_category", period) - p = parameters(period).gov.aca.family_tier_ratings - state_code = tax_unit.household("state_code", period) - in_ny = state_code == state_code.possible_values.NY - - one_adult = where(in_ny, p.ny.ONE_ADULT, p.vt.ONE_ADULT) - two_adults = where(in_ny, p.ny.TWO_ADULTS, p.vt.TWO_ADULTS) - one_adult_with_children = where( - in_ny, - p.ny.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - p.vt.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - ) - two_adults_with_children = where( - in_ny, - p.ny.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - p.vt.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - ) - child_only = where(in_ny, p.ny.CHILD_ONLY, 0) - - base_multiplier = select( - [ - family_category == FamilyTierCategory.ONE_ADULT, - family_category == FamilyTierCategory.TWO_ADULTS, - family_category - == FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - family_category - == FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - family_category == FamilyTierCategory.CHILD_ONLY, - ], - [ - one_adult, - two_adults, - one_adult_with_children, - two_adults_with_children, - child_only, - ], - default=0, - ) - - person = tax_unit.members - pays_premium = person("pays_aca_premium", period) - dependent_child = person("is_aca_family_tier_dependent_child", period) - adult_count = tax_unit.sum(pays_premium & ~dependent_child) - extra_adults = max_(adult_count - 2, 0) - age_29_child = person("is_aca_ny_age_29_dependent_child", period) - age_29_multiplier = where( - tax_unit.any(age_29_child), - parameters(period).gov.aca.ny_age_29_dependent_child_tier_multiplier, - 1, + enrolled = tax_unit.members("pays_aca_premium", period) + return family_tier_multiplier( + tax_unit, period, parameters, family_category, enrolled ) - return base_multiplier * age_29_multiplier + extra_adults * one_adult From 28212117a454b7df52cfba1846fef903ca3e152e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 12:36:17 -0400 Subject: [PATCH 09/32] Apply each state premium program's own rule to an enrollee outside the tax family Massachusetts, Connecticut, Colorado, Maryland, California, New Mexico and Vermont require each enrollee to be eligible for the federal credit, so the gate flows through is_aca_ptc_eligible and their wraps use the coverage family's benchmark. New Mexico's Middle Income Household component and New Jersey Health Plan Savings require every federal criterion except income, so they now read the tax family too. Washington's Cascade Care Savings pays its non-federally-subsidized amount (Group 2) to a QHP-eligible enrollee without a federal subsidy, so a claimable enrollee moves from Group 1 to Group 2. Co-Authored-By: Claude Opus 5.5 --- .../premium_subsidy/ca_premium_subsidy.yaml | 120 +++++++++++++ .../co_premium_assistance.yaml | 162 +++++++++++++++++ .../ct_covered_connecticut.yaml | 162 +++++++++++++++++ .../hic/connector_care/ma_connector_care.yaml | 162 +++++++++++++++++ .../ma_connector_care_member_eligible.yaml | 76 ++++++++ .../md_premium_assistance.yaml | 162 +++++++++++++++++ .../gov/states/nj/dobi/njhps/nj_njhps.yaml | 162 +++++++++++++++++ .../dobi/njhps/nj_njhps_member_eligible.yaml | 76 ++++++++ .../nm_premium_assistance.yaml | 162 +++++++++++++++++ .../nm_premium_assistance_mih_eligible.yaml | 123 +++++++++++++ .../vt_premium_assistance.yaml | 158 +++++++++++++++++ .../wa_cascade_care_savings.yaml | 163 ++++++++++++++++++ ...wa_cascade_care_savings_member_amount.yaml | 77 +++++++++ ..._cascade_care_savings_member_eligible.yaml | 76 ++++++++ .../premium_subsidy/ca_premium_subsidy.py | 7 +- .../ca_premium_subsidy_eligible.py | 7 +- .../co_premium_assistance.py | 9 +- .../ct_covered_connecticut.py | 17 +- .../hic/connector_care/ma_connector_care.py | 8 +- .../md_premium_assistance.py | 7 +- .../gov/states/nj/dobi/njhps/nj_njhps.py | 4 +- .../nj/dobi/njhps/nj_njhps_member_eligible.py | 12 +- .../nm_premium_assistance.py | 8 +- .../nm_premium_assistance_mih_eligible.py | 6 +- .../vt_premium_assistance.py | 12 +- .../wa_cascade_care_savings_member_amount.py | 19 +- ...wa_cascade_care_savings_member_eligible.py | 24 ++- 27 files changed, 1939 insertions(+), 42 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 889da75d6d7..34561143831 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -504,3 +504,123 @@ output: ca_premium_subsidy_eligible: true ca_premium_subsidy: 3_935 + +- name: 2026 CA joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CA + output: + # Program Design (c)(2), (d)(7)(B). 22,000 / 15,650 = 140% FPL; + # federal 3.5724%: aca_ptc = 6,000 - 785.92 = 5,214.08; CA 0%: + # 6,000 - 5,214.08 = 785.92. + ca_premium_subsidy: 785.92 + +- name: 2026 CA joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CA + output: + ca_premium_subsidy: 785.92 + +- name: 2026 CA joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CA + output: + # 104% FPL, 2.1%: aca_ptc = 12,000 - 462 = 11,538; CA 0%: 462. + ca_premium_subsidy: 462 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml index ec7b5568aeb..fdd1995fa79 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml @@ -681,3 +681,165 @@ co_premium_assistance_eligible: true co_premium_assistance: 960 assigned_co_premium_assistance: 0 + +- name: 2026 CO joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # C.R.S. 10-16-1205(1)(b)(II): payments for Coloradans who receive the + # premium tax credit. One member: min(12 * 80, 6,000 - 5,016.40) = 960. + co_premium_assistance: 960 + +- name: 2026 CO joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CO + output: + co_premium_assistance: 960 + +- name: 2026 CO joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + co_premium_assistance: 492.97 + +- name: 2026 CO joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # Neither spouse is in a federal tax family. + co_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml index 8284416c1e3..22c48b87423 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml @@ -371,3 +371,165 @@ state_code: CT output: ct_covered_connecticut: 500 # verified: negative income does not disturb the injected residual + +- name: 2026 CT joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CT + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # Public Act 21-2, Section 16(b)(1): enrollees must be eligible for premium + # and cost-sharing subsidies. 6,000 - 5,016.40 = 983.60. + ct_covered_connecticut: 983.6 + +- name: 2026 CT joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CT + output: + ct_covered_connecticut: 983.6 + +- name: 2026 CT joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CT + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + ct_covered_connecticut: 492.97 + +- name: 2026 CT joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CT + output: + # Neither spouse is in a federal tax family. + ct_covered_connecticut: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml index cf046cd4d31..b52405c1e14 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml @@ -566,3 +566,165 @@ state_code: MA output: ma_connector_care: 500 + +- name: 2026 MA joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # 956 CMR 12.04(3)(a)2: ConnectorCare needs APTC eligibility, so only the + # other spouse counts. Band 2A ($0): 6,000 - 5,016.40 - 0 = 983.60. + ma_connector_care: 983.6 + +- name: 2026 MA joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: MA + output: + ma_connector_care: 983.6 + +- name: 2026 MA joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + ma_connector_care: 492.97 + +- name: 2026 MA joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + # Neither spouse is in a federal tax family. + ma_connector_care: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml index 211a5ca2dc7..27c4ffd05fa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml @@ -57,3 +57,79 @@ ma_connector_care_member_eligible: true person2: ma_connector_care_member_eligible: false + +- name: 2026 MA a spouse who can be claimed elsewhere is not a ConnectorCare member (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + ma_connector_care_member_eligible: [false, true] + +- name: 2026 MA a spouse who can be claimed elsewhere is not a ConnectorCare member (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: MA + output: + ma_connector_care_member_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml index 785e626bc21..fe6090f2e5b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml @@ -723,3 +723,165 @@ state_code: VA output: md_premium_assistance: [868.57, 0] # MD unit derived > 0, VA unit masked to 0 by defined_for + +- name: 2026 MD joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MD + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # COMAR 14.35.21.03A(1): members of an APTC-eligible tax household. + # 0% target at 150% FPL: 6,000 - 5,016.40 = 983.60. + md_premium_assistance: 983.6 + +- name: 2026 MD joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: MD + output: + md_premium_assistance: 983.6 + +- name: 2026 MD joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MD + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + md_premium_assistance: 492.97 + +- name: 2026 MD joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MD + output: + # Neither spouse is in a federal tax family. + md_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml index 5ce199518f4..9982d762b4a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml @@ -792,3 +792,165 @@ state_code: NJ output: nj_njhps: 0 + +- name: 2026 NJ joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # NJHPS goes to enrollees otherwise eligible for APTC: one member at + # $20 PMPM (138-150% FPL) = 240; nobody claimable is 110% FPL, below 138%. + nj_njhps: 240 + +- name: 2026 NJ joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NJ + output: + nj_njhps: 240 + +- name: 2026 NJ joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + nj_njhps: 0 + +- name: 2026 NJ joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + # Neither spouse is in a federal tax family. + nj_njhps: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml index 0aadc7215a3..a17d1e77e50 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml @@ -267,3 +267,79 @@ state_code: NJ output: nj_njhps_member_eligible: false + +- name: 2026 NJ a spouse who can be claimed elsewhere is not an NJHPS member (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + nj_njhps_member_eligible: [false, true] + +- name: 2026 NJ a spouse who can be claimed elsewhere is not an NJHPS member (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NJ + output: + nj_njhps_member_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml index 7852514f67b..3b634cb0983 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml @@ -885,3 +885,165 @@ # Derived SLCSP from the Bernalillo County rating area yields a positive base # top-up. Value is data-driven from the premium table. nm_premium_assistance: 2_645.60 + +- name: 2026 NM joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # MAP manual: consumers must qualify for the federal premium tax credit. + # Gross-up to 200% FPL, 0% target: 6,000 * 1.1 - 5,016.40 = 1,583.60. + nm_premium_assistance: 1583.6 + +- name: 2026 NM joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NM + output: + nm_premium_assistance: 1583.6 + +- name: 2026 NM joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + nm_premium_assistance: 1692.97 + +- name: 2026 NM joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # Neither spouse is in a federal tax family. + nm_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml index 9dc6fda92fb..4a86773a91f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml @@ -237,3 +237,126 @@ state_code: NM output: nm_premium_assistance_mih_eligible: false + +- name: "2026 NM above 400% FPL for a tax family of one: the other spouse qualifies for the MIH component (the claimable spouse is the head)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 70_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # 70,000 / 15,650 = 447% FPL; 6,000 - 0.085 * 70,000 = 50. + aca_magi_fraction: 4.47 + nm_premium_assistance_mih_eligible: true + nm_premium_assistance: 50 + +- name: "2026 NM above 400% FPL for a tax family of one: the other spouse qualifies for the MIH component (the claimable spouse is the spouse)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 70_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NM + output: + aca_magi_fraction: 4.47 + nm_premium_assistance_mih_eligible: true + nm_premium_assistance: 50 + +- name: "2026 NM above 400% FPL with both spouses claimable: no MIH component" + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 70_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # The Addendum requires every federal PTC criterion except income. + nm_premium_assistance_mih_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml index 52b98db3dc1..0623d929169 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml @@ -406,3 +406,161 @@ state_code: VT output: vt_premium_assistance: 0 + +- name: 2026 VT joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: VT + slcsp_age_0: 6000 + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # 33 V.S.A. 1812(a)(1). Family tier ONE_ADULT: benchmark 6,000. + # min(23,475 * 0.015, 6,000 - 5,016.40) = 352.13. + vt_premium_assistance: 352.13 + +- name: 2026 VT joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: VT + slcsp_age_0: 6000 + output: + vt_premium_assistance: 352.13 + +- name: 2026 VT joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: VT + slcsp_age_0: 6000 + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + vt_premium_assistance: 352.13 + +- name: 2026 VT joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: VT + slcsp_age_0: 6000 + output: + # Neither spouse is in a federal tax family. + vt_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml index 986ed843771..5d2dd12d03d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml @@ -749,3 +749,166 @@ state_code: WA output: wa_cascade_care_savings: 3_000 + +- name: 2026 WA joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # Group 2 ($250 PMPM) for the claimable spouse, who is QHP-eligible but + # has no federal subsidy, and Group 1 ($55) for the other: 3,000 + 660; + # cap = 12,000 - 5,016.40 = 6,983.60 (both enrollees' net premium). + wa_cascade_care_savings: 3660 + +- name: 2026 WA joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: WA + output: + wa_cascade_care_savings: 3660 + +- name: 2026 WA joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + wa_cascade_care_savings: 492.97 + +- name: 2026 WA joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # Both spouses are Group 2: 2 * 12 * 250 = 6,000, below the cap of 12,000. + wa_cascade_care_savings: 6000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml index 0352869ffe8..2caf6ea14e0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml @@ -154,3 +154,80 @@ state_code: WA output: wa_cascade_care_savings_member_amount: [660, 3_000, 0, 0] + +- name: 2026 WA the claimable spouse gets the Group 2 amount and the other the Group 1 amount (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # 12 * 250 and 12 * 55. + wa_cascade_care_savings_member_amount: [3000, 660] + +- name: 2026 WA the claimable spouse gets the Group 2 amount and the other the Group 1 amount (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: WA + output: + wa_cascade_care_savings_member_amount: [660, 3000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml index 15b9fdf1d30..e6343dfebfc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml @@ -173,3 +173,79 @@ state_code: WA output: wa_cascade_care_savings_member_eligible: [true, true, false, false] + +- name: 2026 WA a spouse who can be claimed elsewhere is a Group 2 member (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + wa_cascade_care_savings_member_eligible: [true, true] + +- name: 2026 WA a spouse who can be claimed elsewhere is a Group 2 member (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: WA + output: + wa_cascade_care_savings_member_eligible: [true, true] diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py index 32ad4395a41..213e55ae882 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py @@ -31,9 +31,10 @@ def formula(tax_unit, period, parameters): # Clamp income to zero so a negative ACA MAGI cannot inflate the top-up # above the benchmark - APTC residual via the -pct*income term. income = max_(tax_unit("aca_magi", period), 0) - # slcsp is a MONTH-period variable; core sums the 12 months when - # called from this YEAR formula. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the coverage family (Program Design (d)(5) points + # to 26 CFR 1.36B-3(f)); a MONTH variable that core sums over the + # year here. + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) ca_pct = tax_unit("ca_premium_subsidy_applicable_percentage", period) # Eligibility is enforced by defined_for = ca_premium_subsidy_eligible. diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py index e9fd47b9eaf..9e873bcd408 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py @@ -42,7 +42,10 @@ def formula(tax_unit, period, parameters): # must be a filer. This gate replaces reliance on the federal 100% FPL # floor exception: the federal PTC gate admits a below-poverty # immigration exception that the applicable-return-filer definition - # excludes. Dependents claimed by another filer do not form separate - # claiming units in the model, so the dependent exclusion is structural. + # excludes. Program Design (d)(7)(B) allows no assistance to an + # individual claimed as a dependent of another return filer; the + # federal gate already leaves such a person out of the tax family + # (26 U.S.C. 36B(c)(1)(D)), and Program Design (c)(2) requires every + # federal APTC eligibility requirement. is_filer = tax_unit("tax_unit_is_filer", period) return in_effect & aptc_eligible & income_eligible & is_filer diff --git a/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py b/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py index 53bf7dc4fd2..3905017f842 100644 --- a/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py +++ b/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py @@ -60,9 +60,12 @@ def formula(tax_unit, period, parameters): p.amount.first_member + p.amount.additional_member * (n - 1), 0, ) - # slcsp is a MONTH-period variable; core sums the 12 months when called - # from this YEAR formula. Approximates the enrolled-plan premium. - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # core sums over the year here; it approximates the eligible members' + # enrolled-plan premiums. C.R.S. 10-16-1205(1)(b)(II) pays carriers + # for Coloradans who receive the premium tax credit, so an enrollee + # outside the federal tax family is not counted. + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) residual = max_(0, slcsp_annual - aca_ptc) annual = min_(MONTHS_IN_YEAR * flat_monthly, residual) diff --git a/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py b/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py index 1ac3523df95..6f9ea164e88 100644 --- a/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py +++ b/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py @@ -17,9 +17,11 @@ class ct_covered_connecticut(Variable): "Covered Connecticut pays the enrollee's residual benchmark silver-plan " "premium after the federal advance premium tax credit, driving the net " "premium to $0 for eligible enrollees at or below 175% of the federal " - "poverty line. The benchmark second-lowest-cost silver plan premium " - "(slcsp) proxies the benchmark plan and is annualized from its monthly " - "definition. The subsidy is additive on top of the federal APTC and is " + "poverty line. The premium tax credit's benchmark second-lowest-cost " + "silver plan premium for the coverage family (aca_ptc_slcsp) proxies " + "the benchmark plan and is annualized from its monthly definition; it " + "leaves out an enrollee who is not eligible for the federal subsidies " + "because they can be claimed on another return. The subsidy is additive on top of the federal APTC and is " "capped implicitly at the benchmark premium because the federal APTC " "never exceeds it, so the residual is never negative. The program also " "zeroes cost-sharing and adds dental and non-emergency medical " @@ -30,9 +32,12 @@ class ct_covered_connecticut(Variable): ) def formula(tax_unit, period, parameters): - # slcsp is a MONTH-period variable; add() annualizes it to match the - # YEAR-period aca_ptc so both sides of the residual are annual. - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # add() annualizes to match the YEAR-period aca_ptc. Public Act 21-2, + # Section 16(b)(1), covers only people eligible for premium and + # cost-sharing subsidies, so an enrollee outside the federal tax + # family (one who can be claimed on another return) is left out. + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) # Residual benchmark premium after the federal APTC. The max_(0, ...) # floor is defensive and redundant in the baseline, where aca_ptc never diff --git a/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py b/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py index 154b1faf3d2..45f1f0deb3d 100644 --- a/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py +++ b/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py @@ -48,9 +48,11 @@ def formula(tax_unit, period, parameters): magi_fraction = tax_unit("aca_magi_fraction", period) per_person_monthly = p.enrollee_premium.calc(magi_fraction) enrollee_contribution = MONTHS_IN_YEAR * per_person_monthly * n_members - # SLCSP is a MONTH variable summed to the year to proxy the benchmark - # ConnectorCare plan; the full APTC is netted per 956 CMR 12.04(3)(c). - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family (a MONTH variable + # summed to the year) proxies the benchmark ConnectorCare plan of the + # APTC-eligible enrollees (956 CMR 12.04(3)(a)2); the full APTC is + # netted per 956 CMR 12.04(3)(c). + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) subsidy = max_(0, slcsp_annual - aca_ptc - enrollee_contribution) eligible = tax_unit("ma_connector_care_eligible", period) diff --git a/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py b/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py index 28b2835f2c9..e67ff51f223 100644 --- a/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py +++ b/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py @@ -46,9 +46,10 @@ def formula(tax_unit, period, parameters): md_percentage = tax_unit( "md_premium_assistance_target_contribution_percentage", period ) - # slcsp is a MONTH-period variable; core sums the 12 months when - # called from this YEAR formula. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # core sums over the year here: COMAR 14.35.21.03A(1) limits the + # subsidy to members of an APTC-eligible tax filer's tax household. + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) # Premium balance remaining after the federal APTC (COMAR .04B). premium_after_aptc = max_(0, slcsp - aca_ptc) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py index 09adfb9943c..97c660c1ad8 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py @@ -49,7 +49,9 @@ def formula(tax_unit, period, parameters): # plan premium. NJHPS is added atop APTC and cannot lower the net # premium below zero, so it is capped at the post-APTC residual. In the # 400-600% band aca_ptc is 0, so the residual equals slcsp_annual. - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark is the federal coverage family's: NJHPS members must + # be in a tax household otherwise eligible for APTC. + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) residual = max_(0, slcsp_annual - aca_ptc) return min_(base_annual, residual) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py index 6be048c756f..f46e8605147 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py @@ -13,7 +13,10 @@ class nj_njhps_member_eligible(Variable): ) documentation = ( "A person is a New Jersey Health Plan Savings enrollee when they pay an " - "ACA marketplace premium, do not file a separate return, and household " + "ACA marketplace premium, are in the federal premium tax credit tax " + "family (NJHPS goes to enrollees otherwise eligible for APTC, so not " + "someone another taxpayer can claim), do not file a separate return, " + "and household " "income (ACA MAGI per 26 U.S.C. 36B(d)(2)) falls within the 138-600% " "FPL NJHPS band. This deliberately does NOT reuse is_aca_ptc_eligible: " "that variable embeds the federal 400% FPL income cliff (the PY2026 " @@ -45,7 +48,12 @@ def formula(person, period, parameters): # income under the federal PTC floor of 100% FPL, and within_band # already starts at the 138% FPL NJHPS floor. pays_premium = person("pays_aca_premium", period) + # NJHPS goes to enrollees otherwise eligible for APTC, so a person + # outside the federal tax family (one who can be claimed on another + # return, or a dependent of a return with such a filer) is not a + # member (26 U.S.C. 36B(c)(1)(D) is not an income test). + tax_family_member = person("is_aca_tax_family_member", period) fstatus = person.tax_unit("filing_status", period) not_separate = fstatus != fstatus.possible_values.SEPARATE within_band = (magi_fraction >= p.fpl_floor) & (magi_fraction <= p.fpl_limit) - return pays_premium & not_separate & within_band + return pays_premium & tax_family_member & not_separate & within_band diff --git a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py index df33b423c65..877bbee18e6 100644 --- a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py +++ b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py @@ -36,9 +36,11 @@ def formula(tax_unit, period, parameters): # Clamp income to zero so a negative ACA MAGI cannot inflate the top-up # above the benchmark - APTC residual via the -pct*income term. income = max_(tax_unit("aca_magi", period), 0) - # slcsp is a MONTH-period variable; core sums the 12 months when - # called from this YEAR formula. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # core sums over the year here: both components cover only enrollees + # who meet the federal PTC criteria (the base program in full, the + # Middle Income Household component except for income). + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) magi_frac = tax_unit("aca_magi_fraction", period) diff --git a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py index 78a6a44ff95..bbb6133f84d 100644 --- a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py +++ b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py @@ -29,7 +29,11 @@ def formula(tax_unit, period, parameters): # the Medicaid coverage gap, income under the federal PTC floor of 100% # FPL; that cannot bind here, because income_eligible already requires # income above 400% FPL. - pays_premium = add(tax_unit, period, ["pays_aca_premium"]) > 0 + # The Addendum requires every federal PTC criterion except income, so + # the enrollee must also be in the federal tax family (26 U.S.C. + # 36B(c)(1)(D)): is_aca_coverage_family_member is pays_aca_premium + # restricted to that family. + pays_premium = add(tax_unit, period, ["is_aca_coverage_family_member"]) > 0 filing_status = tax_unit("filing_status", period) not_separate = filing_status != filing_status.possible_values.SEPARATE # is_aca_ptc_eligible does not embed the federal PTC filing requirement diff --git a/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py b/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py index b19a3450ccd..ff3d842334d 100644 --- a/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py +++ b/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py @@ -19,9 +19,9 @@ class vt_premium_assistance(Variable): "percentage-point reduction to the federal section 36B applicable " "percentage, so the subsidy equals the enrollee's income times that " "reduction, capped at the benchmark premium remaining after the federal " - "APTC so it never duplicates the federal credit. The benchmark second-" - "lowest-cost silver plan premium (slcsp) is annualized from its monthly " - "definition. At very low income the applicable percentage can fall below " + "APTC so it never duplicates the federal credit. The premium tax " + "credit's benchmark for the coverage family (aca_ptc_slcsp) is " + "annualized from its monthly definition. At very low income the applicable percentage can fall below " "1.5%, in which case the reduction floors at zero and the cap lets " "Vermont Premium Assistance cover the full residual premium after the " "federal APTC. The amount is not taxable. Advance-payment, " @@ -54,8 +54,10 @@ def formula(tax_unit, period, parameters): # Incremental contribution gap between the federal applicable # percentage and the lower Vermont percentage. contribution_gap = max_(0, income * (federal_percentage - vt_percentage)) - # slcsp is a MONTH-period variable; annualize it to match aca_ptc. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, annualized to match + # aca_ptc: 33 V.S.A. 1812(a)(1) covers only those eligible for the + # federal premium tax credit. + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) # Cap at the premium balance remaining after the federal APTC. premium_after_aptc = max_(0, slcsp - aca_ptc) diff --git a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py index 307e9cc6f90..de701b06234 100644 --- a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py +++ b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py @@ -10,6 +10,7 @@ class wa_cascade_care_savings_member_amount(Variable): defined_for = StateCode.WA reference = ( "https://app.leg.wa.gov/rcw/default.aspx?cite=43.71.110", + # PDF pages 1, 5 (the PMPM amounts and the three groups). "https://www.wahbexchange.org/content/dam/materials/communications/legislative/2025/WAHBE_Final_PY_2026_Cascade_Care_Savings_Maximum_Per_Member_Per_Month_Methodology.pdf#page=1", "https://www.wahbexchange.org/content/dam/wahbe-assets/materials/collateral/cc/FinalPY2026CascadeCareSavingsPolicy_Combined.pdf#page=14", ) @@ -22,15 +23,18 @@ class wa_cascade_care_savings_member_amount(Variable): "with-federal-subsidy amount ($55 PMPM for PY2026), and Group 3 " "members without a federally recognized immigration status who lack " "minimum essential coverage through a state medical assistance program " - "receive the without-federal-subsidy amount ($250 PMPM for PY2026). " + "receive the without-federal-subsidy amount ($250 PMPM for PY2026), " + "as do Group 2 members modeled as QHP-eligible enrollees outside the " + "federal tax family (someone another taxpayer can claim). " "Group 3 excludes members eligible for Washington Apple Health " "Expansion (undocumented adults) or Apple Health for Kids (children " "under 19), per Policy Section 4(1)(f), which also avoids double " "counting with the Medicaid-cost proxy in healthcare_benefit_value; it " "further requires the member not be premium-tax-credit-eligible, so " - "Groups 1 and 3 are mutually exclusive. Group 2 (lawfully present but " - "premium-tax-credit-ineligible) is not separately identifiable in the " - "model and is documented away. The monthly amount is annualized by " + "Groups 1 and 3 are mutually exclusive. Other Group 2 members " + "(lawfully present but premium-tax-credit-ineligible for other " + "reasons) are not separately identifiable in the model and are " + "documented away. The monthly amount is annualized by " "MONTHS_IN_YEAR, assuming full-year enrollment." ) @@ -54,6 +58,11 @@ def formula(person, period, parameters): apple_health_expansion = person("wa_apple_health_expansion_eligible", period) apple_health_kids = person("wa_apple_health_kids_eligible", period) group_3 = undocumented & ~apple_health_expansion & ~apple_health_kids & ~group_1 + # Group 2: QHP-eligible enrollees outside the federal tax family, who + # have no federal subsidy (see wa_cascade_care_savings_member_eligible). + group_2 = person("pays_aca_premium", period) & ~person( + "is_aca_tax_family_member", period + ) with_subsidy = where(group_1, p.amount.with_federal_subsidy, 0) - without_subsidy = where(group_3, p.amount.without_federal_subsidy, 0) + without_subsidy = where(group_2 | group_3, p.amount.without_federal_subsidy, 0) return MONTHS_IN_YEAR * (with_subsidy + without_subsidy) diff --git a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py index 1eb0c88c9db..d931c332c49 100644 --- a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py +++ b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py @@ -13,10 +13,14 @@ class wa_cascade_care_savings_member_eligible(Variable): "https://www.wahbexchange.org/content/dam/materials/communications/legislative/2025/WAHBE_Final_PY_2026_Cascade_Care_Savings_Maximum_Per_Member_Per_Month_Methodology.pdf#page=5", ) documentation = ( - "A person is a Cascade Care Savings enrollee if they fall in Group 1 " - "or Group 3. Group 1 members are eligible for the federal ACA premium " + "A person is a Cascade Care Savings enrollee if they fall in Group 1, " + "2 or 3. Group 1 members are eligible for the federal ACA premium " "tax credit (embedding on-Marketplace enrollment, the MFS exclusion, " "immigration/TIN status, and the required-contribution income test). " + "Group 2 members are QHP-eligible but not eligible for the federal " + "credit; the model identifies those outside the federal tax family " + "(someone another taxpayer can claim, or a dependent of a return with " + "such a filer), since Policy Section 4(2) does not exclude them. " "Group 3 members are undocumented residents who lack minimum essential " "coverage through a state medical assistance program: they are excluded " "if eligible for Washington Apple Health Expansion (undocumented adults) " @@ -24,8 +28,9 @@ class wa_cascade_care_savings_member_eligible(Variable): "4(1)(f), which also avoids double counting with the Medicaid-cost " "proxy in healthcare_benefit_value. Group 3 further requires the member " "not be premium-tax-credit-eligible, making Groups 1 and 3 mutually " - "exclusive. Group 2 (lawfully present but premium-tax-credit-" - "ineligible) is not separately identifiable and is documented away." + "exclusive. Other Group 2 members (lawfully present but premium-tax-" + "credit-ineligible for other reasons) are not separately identifiable " + "and are documented away." ) def formula(person, period, parameters): @@ -43,4 +48,13 @@ def formula(person, period, parameters): apple_health_expansion = person("wa_apple_health_expansion_eligible", period) apple_health_kids = person("wa_apple_health_kids_eligible", period) group_3 = undocumented & ~apple_health_expansion & ~apple_health_kids & ~group_1 - return group_1 | group_3 + # Group 2: QHP-eligible enrollees outside the federal tax family. The + # PMPM methodology defines Group 2 as QHP-eligible residents "not + # eligible for APTC Federal subsidies but eligible for CCS", and + # Policy Section 4(2) does not list being claimable among its + # exclusions. pays_aca_premium already excludes undocumented people, + # so Groups 2 and 3 do not overlap. + group_2 = person("pays_aca_premium", period) & ~person( + "is_aca_tax_family_member", period + ) + return group_1 | group_2 | group_3 From ec0aaef87d6b496c9d7079869793b41c83047017 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 12:36:17 -0400 Subject: [PATCH 10/32] Test the premium tax credit invariants for claimable filers Co-Authored-By: Claude Opus 5.5 --- .../test_claimable_filer_ptc_invariants.py | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py new file mode 100644 index 00000000000..c0ddc4c9fc2 --- /dev/null +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -0,0 +1,224 @@ +"""The premium tax credit for a filer who can be claimed as a dependent. + +Someone another taxpayer can claim is not an applicable taxpayer (IRC +36B(c)(1)(D)) and is outside the tax family (26 CFR 1.36B-1(d)(2)); a return +with such a filer claims no dependents (IRC 152(b)(1)). The credit therefore +uses `aca_tax_family_size` for the poverty line and the coverage family's +benchmark `aca_ptc_slcsp`, and the state premium programs that wrap it follow. + +For couples drawn by Hypothesis and a seeded population, with either, both or +neither spouse claimed: + +1. Swap invariance: each output is the same under either head/spouse + labelling. +2. Identities: `aca_tax_family_size` equals the filers who cannot be claimed + plus the dependents when no filer can be claimed; nobody outside the tax + family is eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; a return where every + filer can be claimed has no credit; and a return where no filer can be + claimed keeps `slcsp`, the tax unit size and the tax unit's poverty line. +3. Monotonicity: marking another filer as claimed never raises the tax family + size, never raises the benchmark while the enrollees stay the same, and + never raises the credit of a return that had an eligible member. Both can + rise otherwise, as intended: a couple below 100% of the two-person line can + be above it for a tax family of one, so IRC 36B allows the credit and the + unclaimable spouse leaves the coverage gap that pays_aca_premium assumes + nobody buys into. +""" + +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.tests.core.test_dependent_elsewhere_head_spouse_swap_invariance import ( + CLAIM_PATTERNS, + TOLERANCE, + _calc, + _crafted_cases, + _monotone_variants, + _seeded_couples, + _situation, + _swap_variants, + adults, + dependents, +) + +# Texas (age curve, no expansion), New York and Vermont (family tiers), and +# the states whose premium programs wrap the credit. +STATES = ["TX", "NY", "VT", "MA", "CT", "CO", "MD", "CA", "NM", "NJ", "WA"] +YEARS = [2021, 2022, 2023, 2024, 2025, 2026] +FEDERAL = [ + "aca_tax_family_size", + "aca_fpg", + "aca_magi_fraction", + "slcsp", + "aca_ptc_slcsp", + "aca_ptc", + "marketplace_csr_eligible", +] +STATE_WRAPS = [ + "ma_connector_care", + "ct_covered_connecticut", + "co_premium_assistance", + "md_premium_assistance", + "ca_premium_subsidy", + "nm_premium_assistance", + "vt_premium_assistance", + "nj_njhps", + "wa_cascade_care_savings", +] +OUTPUTS = FEDERAL + STATE_WRAPS + + +@st.composite +def couples(draw): + return { + "state": draw(st.sampled_from(STATES)), + "adults": [draw(adults()), draw(adults())], + "dependents": draw(st.lists(dependents(), max_size=2)), + "claimed": draw(st.sampled_from(CLAIM_PATTERNS)), + } + + +def _ptc_cases(states): + # Two 30-year-olds whose income is 150% of the one-person line in 2025, + # with and without a child: the credit for the unclaimable spouse. + adult = {"age": 30, "employment_income": 11_295.0} + cases = [] + for state in states: + for claimed in [(True, False), (True, True)]: + cases += [ + { + "state": state, + "adults": [adult, adult], + "dependents": [], + "claimed": claimed, + }, + { + "state": state, + "adults": [adult, adult], + "dependents": [{"age": 4}], + "claimed": claimed, + }, + ] + return cases + + +def _tax_unit_sum(sim, name, year): + return np.asarray(sim.calculate(name, year, map_to="tax_unit"), dtype=float) + + +def _identities(sim, year): + claimed_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + independent = _calc(sim, "head_spouse_count_not_dependent_elsewhere", year) + dependents_count = _calc(sim, "tax_unit_count_dependents", year) + size = _calc(sim, "aca_tax_family_size", year) + np.testing.assert_array_equal( + size, independent + np.where(claimed_filer, 0, dependents_count) + ) + eligible = np.asarray(sim.calculate("is_aca_ptc_eligible", year)) + member = np.asarray(sim.calculate("is_aca_tax_family_member", year)) + assert not (eligible & ~member).any(), "an eligible person is outside the family" + slcsp = _calc(sim, "slcsp", year) + benchmark = _calc(sim, "aca_ptc_slcsp", year) + assert (benchmark >= -TOLERANCE).all() + assert (benchmark <= slcsp + TOLERANCE).all(), "benchmark above slcsp" + nobody_in_family = size == 0 + assert not _calc(sim, "aca_ptc", year)[nobody_in_family].any() + unclaimed = ~claimed_filer + np.testing.assert_allclose(benchmark[unclaimed], slcsp[unclaimed], atol=TOLERANCE) + np.testing.assert_array_equal( + size[unclaimed], _calc(sim, "tax_unit_size", year)[unclaimed] + ) + np.testing.assert_allclose( + _calc(sim, "aca_fpg", year)[unclaimed], + _calc(sim, "tax_unit_fpg", year - 1)[unclaimed], + ) + + +def check_swap(units, year, outputs, require_coverage=False): + sim = Simulation(situation=_situation(units, year, _swap_variants)) + if require_coverage: + # The batch must reach the rule: returns with exactly one claimable + # filer that still get a credit and, from 2026 when the modeled state + # programs start, a state wrap paid on one. + one_claimed = ( + _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + ) & (_calc(sim, "head_spouse_count_not_dependent_elsewhere", year) == 1) + assert (_calc(sim, "aca_ptc", year)[one_claimed] > 0).any() + if year >= 2026: + wraps = sum(_calc(sim, name, year) for name in STATE_WRAPS) + assert (wraps[one_claimed] > 0).any() + values = {name: _calc(sim, name, year) for name in outputs} + values["eligible members"] = _tax_unit_sum(sim, "is_aca_ptc_eligible", year) + for name, value in values.items(): + assert np.isfinite(value).all(), f"{name} in {year} is not finite" + drawn, swapped = value[0::2], value[1::2] + differs = ~(np.abs(swapped - drawn) <= TOLERANCE) + assert not differs.any(), ( + f"{name} in {year} changes when the head and spouse labels are " + "exchanged: " + + ", ".join( + f"{units[i]['state']} claimed={units[i]['claimed']} " + f"{drawn[i]:.2f} -> {swapped[i]:.2f}" + for i in np.flatnonzero(differs)[:10] + ) + ) + _identities(sim, year) + + +def check_monotone(units, year): + sim = Simulation(situation=_situation(units, year, _monotone_variants)) + had_eligible = (_tax_unit_sum(sim, "is_aca_ptc_eligible", year) > 0).reshape(-1, 4) + enrollees = _tax_unit_sum(sim, "pays_aca_premium", year).reshape(-1, 4) + for name, guard in ( + ("aca_tax_family_size", None), + ("aca_ptc_slcsp", "same enrollees"), + ("aca_ptc", "had eligible"), + ): + values = _calc(sim, name, year).reshape(-1, 4) + nobody, first, second, both = values.T + for more, fewer, base, label in ( + (first, nobody, 0, "first adult"), + (second, nobody, 0, "second adult"), + (both, first, 1, "both after first"), + (both, second, 2, "both after second"), + ): + rises = more > fewer + TOLERANCE + if guard == "had eligible": + rises &= had_eligible[:, base] + elif guard == "same enrollees": + more_index = {"first adult": 1, "second adult": 2}.get(label, 3) + rises &= enrollees[:, base] == enrollees[:, more_index] + assert not rises.any(), ( + f"{name} in {year} rises when the {label} is claimed " + "elsewhere: " + + ", ".join( + f"{units[i]['state']} {fewer[i]:.2f} -> {more[i]:.2f}" + for i in np.flatnonzero(rises)[:10] + ) + ) + _identities(sim, year) + + +@settings( + max_examples=4, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=15)) +def test_claimable_filer_ptc_is_label_free(year, units): + check_swap(units, year, OUTPUTS) + + +@pytest.mark.parametrize("year", [2025, 2026]) +def test_claimable_filer_ptc_seeded_population(year): + units = ( + _seeded_couples(STATES, per_state=2) + + _crafted_cases(STATES, review_case_only=True) + + _ptc_cases(STATES) + ) + check_swap(units, year, OUTPUTS, require_coverage=True) + check_monotone(units, year) From 948f6bed236abde7fb8d65f73e56ef2a96822f46 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 05:45:13 -0400 Subject: [PATCH 11/32] Cite 36B(c)(1)(D) and 1.36B-2(b)(3) for what each says Co-Authored-By: Claude Opus 5.5 --- .../variables/gov/aca/eligibility/is_aca_ptc_eligible.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py index 34e4cc60e67..88cdd68742d 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py @@ -24,9 +24,10 @@ def formula(person, period, parameters): ) is_income_eligible = standard_income_eligible | below_fpl_exception - # Someone another taxpayer can claim is not an applicable taxpayer - # (26 U.S.C. 36B(c)(1)(D)) and is outside the tax family, along with - # the dependents of a return that has such a filer. + # No credit is allowed to someone another taxpayer can claim (26 + # U.S.C. 36B(c)(1)(D)); they are not an applicable taxpayer (26 CFR + # 1.36B-2(b)(3)) and are outside the tax family, along with the + # dependents of a return that has such a filer (IRC 152(b)(1)). tax_family_member = person("is_aca_tax_family_member", period) return ( From 4597c55a869b45c17376673fbe4aa70f72ca0f3c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 09:35:20 -0400 Subject: [PATCH 12/32] Address the federal claimable-filer review - Child and dependent care: childcare expenses do not count on a return with a claimable filer unless a child under 13 still qualifies (IRC 21(b)(2)(A)(ii)). - Tuition and fees deduction: not allowed to a filer who can be claimed (IRC 222(c)(3)), so the education credit bar no longer makes such a filer eligible. - Recovery rebates: the Armed Forces exception waives the identification rule only, not the exclusion of a spouse who is another taxpayer's dependent. - Georgia: the contributed refundable credit and SB 520 reforms count the same children as ga_ctc_potential through a new ga_ctc_eligible_child. - Education credits and student loan interest read the claimed input directly, since IRC 25A(g)(3) and 221(c) turn on an actual claim. - One reference per source document; quote IRC 24(h)(4)(A) exactly. - Tests: mixed-recipient care, tuition deduction, military rebates, Georgia refund cap, and the swapped New Mexico rebate case; the property test now covers the state readers of the exemption count. Co-Authored-By: Claude Opus 5.5 --- changelog.d/claimable-filer-federal.fixed.md | 2 +- .../reforms/states/ga/ctc/ga_ctc_reform.py | 7 +- .../states/ga/sb520/ga_sb520_reform.py | 8 +- ...test_claimable_filer_federal_invariants.py | 42 +++-- .../credits/cdcc/cdcc_relevant_expenses.yaml | 169 ++++++++++++++++++ .../recovery_rebate_credit.yaml | 154 +++++++++++++++- .../tuition_and_fees_deduction_eligible.yaml | 90 ++++++++++ ...m_low_income_comprehensive_tax_rebate.yaml | 33 +++- .../contrib/states/ga/ctc/ga_ctc_reform.yaml | 90 ++++++++++ .../gov/irs/credits/cdcc/cdcc_eligible.py | 4 +- .../credits/cdcc/cdcc_relevant_expenses.py | 13 ++ .../ctc/maximum/ctc_qualifying_child.py | 2 +- .../ctc_adult_individual_maximum.py | 2 +- ...ligible_for_american_opportunity_credit.py | 14 +- ...s_eligible_for_lifetime_learning_credit.py | 14 +- .../rrc_adult_count_with_valid_ssn.py | 3 +- .../recovery_rebate_credit/rrc_arpa.py | 10 +- .../credits/recovery_rebate_credit/rrc_caa.py | 10 +- .../recovery_rebate_credit/rrc_cares.py | 10 +- .../student_loan_interest_ald_eligible.py | 13 +- .../tuition_and_fees_deduction_eligible.py | 7 +- .../credits/ctc/ga_ctc_eligible_child.py | 29 +++ .../income/credits/ctc/ga_ctc_potential.py | 16 +- 23 files changed, 673 insertions(+), 69 deletions(-) create mode 100644 policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py diff --git a/changelog.d/claimable-filer-federal.fixed.md b/changelog.d/claimable-filer-federal.fixed.md index 545d474e1a6..82a77ce8992 100644 --- a/changelog.d/claimable-filer-federal.fixed.md +++ b/changelog.d/claimable-filer-federal.fixed.md @@ -1 +1 @@ -Apply the federal rules for a filer who can be claimed as a dependent on another return: no child tax credit, credit for other dependents or under-13 care qualifying child on such a return (IRC 24, 21, 152(b)(1)), no personal exemption for that filer (IRC 151(d)(2)) in the exemption count Rhode Island, Delaware, Ohio, Wisconsin, Oregon, Hawaii and New Mexico read, no recovery rebate for that filer, and no education credits or student loan interest deduction on a return with a claimed filer (IRC 25A(g)(3), 221(c)). +Apply the federal rules for a filer who can be claimed as a dependent on another return: no child tax credit, credit for other dependents or under-13 care qualifying child (or that child's care expenses) on such a return (IRC 24, 21, 152(b)(1)), no personal exemption for that filer (IRC 151(d)(2)) in the exemption count Rhode Island, Delaware, Ohio, Wisconsin, Oregon, Hawaii and New Mexico read, no recovery rebate for that filer, including under the Armed Forces exception, no tuition and fees deduction (IRC 222(c)(3)), and no education credits or student loan interest deduction on a return with a claimed filer (IRC 25A(g)(3), 221(c)). diff --git a/policyengine_us/reforms/states/ga/ctc/ga_ctc_reform.py b/policyengine_us/reforms/states/ga/ctc/ga_ctc_reform.py index 6ff5bf1eb8a..8b8f54bc4c6 100644 --- a/policyengine_us/reforms/states/ga/ctc/ga_ctc_reform.py +++ b/policyengine_us/reforms/states/ga/ctc/ga_ctc_reform.py @@ -33,12 +33,7 @@ def formula(tax_unit, period, parameters): unused_credit = max_(potential - non_refundable, 0) # Cap the refund at amount per eligible child (same child count as # ga_ctc_potential: qualifying children under the age threshold). - baseline = parameters(period).gov.states.ga.tax.income.credits.ctc - person = tax_unit.members - age = person("age", period) - ctc_eligible_child = person("ctc_qualifying_child", period) - ga_child_age_eligible = age < baseline.age_threshold - eligible_children = tax_unit.sum(ctc_eligible_child & ga_child_age_eligible) + eligible_children = add(tax_unit, period, ["ga_ctc_eligible_child"]) refund_limit = p.refundable.amount * eligible_children refundable_credit = min_(unused_credit, refund_limit) # Only pay the refund in periods where the reform is in effect. The diff --git a/policyengine_us/reforms/states/ga/sb520/ga_sb520_reform.py b/policyengine_us/reforms/states/ga/sb520/ga_sb520_reform.py index c6b4cfb0fc5..cabfb410d6c 100644 --- a/policyengine_us/reforms/states/ga/sb520/ga_sb520_reform.py +++ b/policyengine_us/reforms/states/ga/sb520/ga_sb520_reform.py @@ -89,12 +89,8 @@ def formula(tax_unit, period, parameters): sb520_active = p_sb520.in_effect - person = tax_unit.members - age = person("age", period) - ctc_eligible_child = person("ctc_qualifying_child", period) - # SB 520 preserves baseline GA CTC age threshold (under 6) - ga_child_age_eligible = age < p.age_threshold - eligible_children = tax_unit.sum(ctc_eligible_child & ga_child_age_eligible) + # SB 520 preserves the baseline GA CTC child test (under 6) + eligible_children = add(tax_unit, period, ["ga_ctc_eligible_child"]) sb520_amount = p_sb520.credits.ctc.amount baseline_amount = p.amount diff --git a/policyengine_us/tests/core/test_claimable_filer_federal_invariants.py b/policyengine_us/tests/core/test_claimable_filer_federal_invariants.py index 58eb58887b4..aab5f3a0156 100644 --- a/policyengine_us/tests/core/test_claimable_filer_federal_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_federal_invariants.py @@ -12,18 +12,24 @@ - a child under 13 is not a child and dependent care qualifying person on such a return (a disabled person still is, IRC 21(b)(1)(B)); - recovery rebates leave out a claimable filer and, on such a return, the - dependents. + dependents; +- the state amounts that read the exemption count follow it (Georgia's two + credits keep their own count). -For couples drawn by Hypothesis and a seeded population, with either, both or -neither spouse claimed, in 2021-2026: +For couples with either, both or neither spouse claimed, in 2021-2026: 1. Swap invariance: each output is the same under either head/spouse - labelling. + labelling (Hypothesis batches and the seeded population). 2. Monotonicity: marking another filer as claimed never raises any of the - amounts or counts below. + amounts or counts below (seeded population only). 3. Identities: `exemptions_count` equals the filers who cannot be claimed plus the dependents when no filer can be claimed, and a return with a claimable - filer has no CTC-qualifying child and no credit for other dependents. + filer has no CTC-qualifying child and no credit for other dependents + (both populations). + +The generated adults have no tuition or student loan interest, so the education +credit, tuition deduction and student loan interest gates are covered by YAML +cases only, as are the contributed reforms. """ import numpy as np @@ -48,22 +54,30 @@ # Texas has no income tax; the others read the federal exemption count. STATES = ["TX", "OR", "HI", "NM", "GA", "RI", "DE", "OH", "WI"] -OUTPUTS = [ - "income_tax", - "ctc_maximum", - "ctc_qualifying_children", - "capped_count_cdcc_eligible", - "exemptions_count", - "rrc_arpa", +# State amounts that read the federal exemption count; each is zero outside +# its state. +STATE_OUTPUTS = [ + "ri_exemptions", + "de_personal_credit_potential", + "oh_personal_exemptions", + "wi_base_exemption", + "or_regular_exemptions", + "hi_regular_exemptions", + "nm_low_and_middle_income_exemption", + "nm_low_income_comprehensive_tax_rebate", + "ga_ctc_potential", + "ga_low_income_credit_potential", ] # Amounts that marking another filer as claimed must never raise. MONOTONE = [ "ctc_maximum", "ctc_qualifying_children", "capped_count_cdcc_eligible", + "cdcc_relevant_expenses", "exemptions_count", "rrc_arpa", -] +] + STATE_OUTPUTS +OUTPUTS = ["income_tax"] + MONOTONE @st.composite diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml index 8ed8c1ecdd6..15dfeb1fe4a 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml @@ -40,3 +40,172 @@ min_head_spouse_earned: 10_000 output: cdcc_relevant_expenses: 3_000 # 1_000 childcare + 2_000 adult care + +# IRC 21(b)(2)(A)(ii): only expenses for the care of a qualifying individual count. +# The child of a return on which a filer can be claimed is not one (IRC 152(b)(1)), +# while the incapacitated relative still is (IRC 21(b)(1)(B)). +- name: 2025 childcare for the child of a claimable single filer does not count + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 5000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + relative: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, child, relative] + tax_unit_childcare_expenses: 3000 + households: + household: + members: [person_a, child, relative] + state_code: TX + output: + count_cdcc_eligible: 1 + cdcc_relevant_expenses: 0 + +- name: 2025 childcare for the child of a joint return with one claimable spouse does not count (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 5000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + employment_income: 5000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + relative: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child, relative] + filing_status: JOINT + tax_unit_childcare_expenses: 3000 + households: + household: + members: [person_a, person_b, child, relative] + state_code: TX + output: + count_cdcc_eligible: 1 + cdcc_relevant_expenses: 0 + +- name: 2025 childcare for the child of a joint return with one claimable spouse does not count (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + employment_income: 5000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + employment_income: 5000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + relative: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child, relative] + filing_status: JOINT + tax_unit_childcare_expenses: 3000 + households: + household: + members: [person_b, person_a, child, relative] + state_code: TX + output: + count_cdcc_eligible: 1 + cdcc_relevant_expenses: 0 + +- name: 2025 care expenses for the incapacitated relative of a claimable filer still count + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 5000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + relative: + age: 40 + is_incapable_of_self_care: true + pre_subsidy_care_expenses: 2000 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, child, relative] + tax_unit_childcare_expenses: 3000 + households: + household: + members: [person_a, child, relative] + state_code: TX + output: + cdcc_relevant_expenses: 2000 + +- name: 2025 the same household with nobody claimable counts the childcare + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 5000 + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + relative: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, child, relative] + tax_unit_childcare_expenses: 3000 + households: + household: + members: [person_a, child, relative] + state_code: TX + output: + count_cdcc_eligible: 2 + cdcc_relevant_expenses: 3000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml index 0d21682feaf..045236d3234 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml @@ -48,7 +48,6 @@ output: recovery_rebate_credit: 3_920 # https://www.efile.com/stimulus-check-three/ - - name: 2021 single filer who can be claimed as a dependent gets no ARPA rebate period: 2021 input: @@ -159,3 +158,156 @@ output: # 3 x $1,400. rrc_arpa: 4_200 + +# The Armed Forces exception (IRC 6428B(e)(2)(E)) waives the identification rule +# for the other spouse; it does not make a dependent of another taxpayer an +# eligible individual (IRC 6428B(c)(2)). +- name: 2021 military joint return with one claimable spouse gets one ARPA rebate (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 30 + is_military: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + rrc_arpa: 1400 + +- name: 2021 military joint return with one claimable spouse gets one ARPA rebate (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_military: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + rrc_arpa: 1400 + +- name: 2021 military joint return whose other spouse is claimable gets one ARPA rebate (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_military: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + rrc_arpa: 1400 + +- name: 2021 military joint return whose other spouse is claimable gets one ARPA rebate (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 30 + is_military: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + rrc_arpa: 1400 + +- name: 2021 military joint return with both spouses claimable gets no ARPA rebate + period: 2021 + input: + people: + person_a: + age: 30 + is_military: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + rrc_arpa: 0 + +- name: 2021 military joint return with neither spouse claimable gets both ARPA rebates + period: 2021 + input: + people: + person_a: + age: 30 + is_military: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + rrc_arpa: 2800 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml index 4912f0155de..dd6bcd8d147 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml @@ -26,3 +26,93 @@ output: tuition_and_fees_deduction_eligible: false # Can't claim this deduction if the household has taken the American Oppportunity or Lifetime Learning Credit. + +# IRC 222(c)(3): no deduction to an individual for whom a section 151 deduction +# may be allowed to another taxpayer. +- name: 2020 single filer who can be claimed as a dependent is not eligible + period: 2020 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + households: + household: + members: [person_a] + state_code: TX + output: + tuition_and_fees_deduction_eligible: false + +- name: 2020 joint return with one claimable spouse is not eligible (the claimable spouse is the head) + period: 2020 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 22 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + tuition_and_fees_deduction_eligible: false + +- name: 2020 joint return with one claimable spouse is not eligible (the claimable spouse is the spouse) + period: 2020 + input: + people: + person_b: + age: 22 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + tuition_and_fees_deduction_eligible: false + +- name: 2020 joint return with neither spouse claimable is eligible + period: 2020 + input: + people: + person_a: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 22 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + tuition_and_fees_deduction_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml index 6c7e83fb126..84ae85e14bf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/rebates/low_income_comprehensive/nm_low_income_comprehensive_tax_rebate.yaml @@ -305,20 +305,24 @@ # claimant (NMSA 7-2-14(A)). nm_low_income_comprehensive_tax_rebate: 0 -- name: 2025 joint return with one claimable spouse keeps the rebate with the couple's combined income +- name: 2025 joint return with one claimable spouse keeps the rebate with the couple's combined income (the claimable spouse is the head) period: 2025 input: people: person_a: age: 30 claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false person_b: age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true tax_units: tax_unit: members: [person_a, person_b] filing_status: JOINT - nm_modified_gross_income: 8_500 + nm_modified_gross_income: 8500 households: household: members: [person_a, person_b] @@ -326,3 +330,28 @@ output: # One exemption; the $8,000-$9,000 row of the one-exemption table. nm_low_income_comprehensive_tax_rebate: 212 + +- name: 2025 joint return with one claimable spouse keeps the rebate with the couple's combined income (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + nm_modified_gross_income: 8500 + households: + household: + members: [person_b, person_a] + state_code: NM + output: + nm_low_income_comprehensive_tax_rebate: 212 diff --git a/policyengine_us/tests/policy/contrib/states/ga/ctc/ga_ctc_reform.yaml b/policyengine_us/tests/policy/contrib/states/ga/ctc/ga_ctc_reform.yaml index 77fcd5db26d..c236fb692d2 100644 --- a/policyengine_us/tests/policy/contrib/states/ga/ctc/ga_ctc_reform.yaml +++ b/policyengine_us/tests/policy/contrib/states/ga/ctc/ga_ctc_reform.yaml @@ -127,3 +127,93 @@ # $10k liability fully absorbs the $250 nonrefundable credit, leaving # nothing unused to refund. ga_refundable_ctc: 0 + +# The refund cap counts the same children as ga_ctc_potential (Georgia's own +# IRC 24(c) child test), not children allowed the federal credit. +- name: Refund cap keeps the child of a filer who can be claimed as a dependent + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.states.ga.ctc.ga_ctc_reform.ga_ctc_reform + input: + gov.contrib.states.ga.ctc.refundable.in_effect: true + people: + parent: + age: 30 + claimed_as_dependent_on_another_return: true + child: + age: 2 + tax_units: + tax_unit: + members: [parent, child] + filing_status: SINGLE + ga_income_tax_before_non_refundable_credits: 0 + households: + household: + members: [parent, child] + state_code: GA + output: + ctc_qualifying_child: [false, false] + ga_ctc_potential: 250 + ga_refundable_ctc: 250 + +- name: Refund cap keeps the child of a joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.states.ga.ctc.ga_ctc_reform.ga_ctc_reform + input: + gov.contrib.states.ga.ctc.refundable.in_effect: true + people: + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 2 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + ga_income_tax_before_non_refundable_credits: 0 + households: + household: + members: [person_a, person_b, child] + state_code: GA + output: + ga_ctc_potential: 250 + ga_refundable_ctc: 250 + +- name: Refund cap keeps the child of a joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.states.ga.ctc.ga_ctc_reform.ga_ctc_reform + input: + gov.contrib.states.ga.ctc.refundable.in_effect: true + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 2 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + ga_income_tax_before_non_refundable_credits: 0 + households: + household: + members: [person_b, person_a, child] + state_code: GA + output: + ga_ctc_potential: 250 + ga_refundable_ctc: 250 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py index 20d82cc32e0..7e45d74c8f1 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py @@ -27,7 +27,9 @@ def formula(person, period, parameters): # A child qualifies only as the filer's dependent ("Your qualifying # child who is your dependent", Publication 503), and under IRC # 152(b)(1) a return on which the filer (or, if joint, either spouse) - # can be claimed as a dependent has no dependents. + # can be claimed as a dependent has no dependents. The model has no + # input for an IRC 152(e) release, so the 21(e)(5) child of a + # custodial parent who can be claimed is not kept. filer_is_dependent = person.tax_unit( "head_or_spouse_is_dependent_elsewhere", period ) diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py index 797f2bbcf39..501d124e5b6 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py @@ -19,6 +19,19 @@ def formula(tax_unit, period, parameters): # individual of any age — a disabled adult dependent or spouse # (care_expenses, a person-level yearly input). childcare = tax_unit("tax_unit_childcare_expenses", period) + # IRC 21(b)(2)(A)(ii) counts only expenses "for the care of a + # qualifying individual". A return on which a filer can be claimed as + # a dependent has no qualifying child under 13 unless the child is + # incapable of self-care, so its childcare expenses do not count. + p = parameters(period).gov.irs.credits.cdcc.eligibility + person = tax_unit.members + qualifying_child = person("is_cdcc_eligible", period) & ( + person("age", period) < p.child_age + ) + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + childcare = where( + dependent_filer & ~tax_unit.any(qualifying_child), 0, childcare + ) adult_care = add(tax_unit, period, ["care_expenses"]) expenses = childcare + adult_care cdcc_limit = tax_unit("cdcc_limit", period) diff --git a/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py b/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py index bd478b60870..3bc002458d2 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py @@ -9,7 +9,7 @@ class ctc_qualifying_child(Variable): definition_period = YEAR defined_for = "is_tax_unit_dependent" reference = ( - "https://www.law.cornell.edu/uscode/text/26/24#a", + # Subsections (a) and (c). "https://www.law.cornell.edu/uscode/text/26/24#c", "https://www.law.cornell.edu/uscode/text/26/152#b_1", # Publication 501, Dependent Taxpayer Test. diff --git a/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py b/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py index 70f5b877edd..23c93da7ba3 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py @@ -25,7 +25,7 @@ def formula(person, period, parameters): filer_meets_tin_requirement = person.tax_unit( "filer_meets_ctc_identification_requirements", period ) - # IRC 24(h)(4) gives the credit for "any dependent of the taxpayer"; + # IRC 24(h)(4)(A) gives the credit for "each dependent of the taxpayer"; # under IRC 152(b)(1) a return on which the filer (or, if joint, # either spouse) can be claimed as a dependent has no dependents. filer_is_dependent = person.tax_unit( diff --git a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py index c3a557b0d8f..91dd0cdffa3 100644 --- a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py @@ -18,11 +18,15 @@ def formula(person, period, parameters): filing_status = tax_unit("filing_status", period) filing_status_values = filing_status.possible_values filing_status_eligible = filing_status != filing_status_values.SEPARATE - # IRC 25A(g)(3): no credit to an individual claimed as a dependent by - # another taxpayer, and the Form 1040 instructions extend this to - # "your spouse if filing jointly", so either filer being claimed bars - # the return's credit. - filer_claimed = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + # IRC 25A(g)(3): no credit to an individual for whom a section 151 + # deduction "is allowed to another taxpayer", and the Form 1040 + # instructions extend this to "your spouse if filing jointly", so + # either filer being claimed bars the return's credit. This turns on + # an actual claim (26 CFR 1.25A-1(f)), so it reads the claimed input + # rather than a claimability helper. + filer = person("is_tax_unit_head_or_spouse", period) + claimed = person("claimed_as_dependent_on_another_return", period) + filer_claimed = tax_unit.any(filer & claimed) requires_1098_t = aoc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( person("has_american_opportunity_credit_1098_t_or_exception", period) diff --git a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py index 75c2e133d15..0dba195cf08 100644 --- a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py @@ -19,11 +19,15 @@ def formula(person, period, parameters): filing_status = tax_unit("filing_status", period) filing_status_values = filing_status.possible_values filing_status_eligible = filing_status != filing_status_values.SEPARATE - # IRC 25A(g)(3): no credit to an individual claimed as a dependent by - # another taxpayer, and the Form 1040 instructions extend this to - # "your spouse if filing jointly", so either filer being claimed bars - # the return's credit. - filer_claimed = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + # IRC 25A(g)(3): no credit to an individual for whom a section 151 + # deduction "is allowed to another taxpayer", and the Form 1040 + # instructions extend this to "your spouse if filing jointly", so + # either filer being claimed bars the return's credit. This turns on + # an actual claim (26 CFR 1.25A-1(f)), so it reads the claimed input + # rather than a claimability helper. + filer = person("is_tax_unit_head_or_spouse", period) + claimed = person("claimed_as_dependent_on_another_return", period) + filer_claimed = tax_unit.any(filer & claimed) requires_1098_t = llc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( person("has_lifetime_learning_credit_1098_t_or_exception", period) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py index 4e08716df78..7ef7231bb3b 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py @@ -13,7 +13,6 @@ class rrc_adult_count_with_valid_ssn(Variable): "https://www.law.cornell.edu/uscode/text/26/6428A#g_2", "https://www.law.cornell.edu/uscode/text/26/6428B#e_2_A", "https://www.law.cornell.edu/uscode/text/26/6428B#e_2_B", - "https://www.law.cornell.edu/uscode/text/26/6428#d_2", ) def formula(tax_unit, period, parameters): @@ -24,6 +23,6 @@ def formula(tax_unit, period, parameters): # An "eligible individual" excludes "any individual with respect to # whom a deduction under section 151 is allowable to another # taxpayer" (IRC 6428(d)(2), 6428A(d)(2)) or "who is a dependent of - # another taxpayer" (IRC 6428B(c)(2)). + # another taxpayer" (IRC 6428B(c)(2)): a claimability test. claimed = person("claimed_as_dependent_on_another_return", period) return tax_unit.sum(head_or_spouse & has_valid_ssn & ~claimed) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py index 252017448f7..2ea2796ed3c 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py @@ -26,9 +26,17 @@ def formula(tax_unit, period, parameters): ) is_joint = tax_unit("tax_unit_is_joint", period) adults_with_ssn = tax_unit("rrc_adult_count_with_valid_ssn", period) + # The exception waives the identification rule, not the exclusion of + # a spouse who is another taxpayer's dependent. + person = tax_unit.members + dependent_filers = tax_unit.sum( + person("is_tax_unit_head_or_spouse", period) + & person("claimed_as_dependent_on_another_return", period) + ) count_adults = where( armed_forces_exception, - 2, # Joint filers always have 2 adults (structural constant) + # Joint filers always have 2 adults (structural constant) + max_(2 - dependent_filers, 0), where(is_joint, adults_with_ssn, min_(adults_with_ssn, 1)), ) max_payment = ( diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py index 577228072bb..cc704eb482b 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py @@ -23,9 +23,17 @@ def formula(tax_unit, period, parameters): "rrc_qualifies_for_armed_forces_exception", period ) is_joint = tax_unit("tax_unit_is_joint", period) + # The exception waives the identification rule, not the exclusion of + # a spouse who is another taxpayer's dependent. + person = tax_unit.members + dependent_filers = tax_unit.sum( + person("is_tax_unit_head_or_spouse", period) + & person("claimed_as_dependent_on_another_return", period) + ) count_adults = where( armed_forces_exception, - 2, # Joint filers always have 2 adults (structural constant) + # Joint filers always have 2 adults (structural constant) + max_(2 - dependent_filers, 0), where(is_joint, adults_with_ssn, min_(adults_with_ssn, 1)), ) # Per 26 USC 6428A(g)(3), children count only if: diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py index bbbf38b4a21..be4a6d8c1c0 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py @@ -23,9 +23,17 @@ def formula(tax_unit, period, parameters): "rrc_qualifies_for_armed_forces_exception", period ) is_joint = tax_unit("tax_unit_is_joint", period) + # The exception waives the identification rule, not the exclusion of + # a spouse who is another taxpayer's dependent. + person = tax_unit.members + dependent_filers = tax_unit.sum( + person("is_tax_unit_head_or_spouse", period) + & person("claimed_as_dependent_on_another_return", period) + ) count_adults = where( armed_forces_exception, - 2, # Joint filers always have 2 adults (structural constant) + # Joint filers always have 2 adults (structural constant) + max_(2 - dependent_filers, 0), where(is_joint, adults_with_ssn, min_(adults_with_ssn, 1)), ) # Per 26 USC 6428(g)(1)(C), children count only if: diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py index 3b45af22df5..fd1bcc2bab0 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_eligible.py @@ -20,11 +20,14 @@ class student_loan_interest_ald_eligible(Variable): def formula(person, period, parameters): head_or_spouse = person("is_tax_unit_head_or_spouse", period) - # Per IRC § 221(c), no deduction for an individual claimed as a - # dependent by another taxpayer; the Form 1040 instructions require - # that "you, or your spouse if filing jointly, aren't claimed as a - # dependent", so either filer being claimed bars the return. - filer_claimed = person.tax_unit("head_or_spouse_is_dependent_elsewhere", period) + # Per IRC § 221(c), no deduction for an individual for whom a section + # 151 deduction "is allowed to another taxpayer"; the Form 1040 + # instructions require that "you, or your spouse if filing jointly, + # aren't claimed as a dependent", so either filer being claimed bars + # the return. This turns on an actual claim, so it reads the claimed + # input rather than a claimability helper. + claimed = person("claimed_as_dependent_on_another_return", period) + filer_claimed = person.tax_unit.any(head_or_spouse & claimed) # Per IRC § 221(e)(2), taxpayer cannot file as Married Filing Separately filing_status = person.tax_unit("filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.py index ac202b88a48..0b169af9c13 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.py @@ -27,4 +27,9 @@ def formula(tax_unit, period, parameters): "lifetime_learning_credit_potential", ], ) - return ~separate & (aoc_llc == 0) + # IRC 222(c)(3): no deduction to an individual for whom a section 151 + # deduction "may be allowed to another taxpayer". Without this, a + # filer barred from the education credits for that reason would + # become eligible here. + dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + return ~separate & (aoc_llc == 0) & ~dependent_filer diff --git a/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py new file mode 100644 index 00000000000..dae5a093b99 --- /dev/null +++ b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class ga_ctc_eligible_child(Variable): + value_type = bool + entity = Person + label = "Eligible child for the Georgia Child Tax Credit" + definition_period = YEAR + defined_for = StateCode.GA + reference = ( + "https://legiscan.com/GA/text/HB136/id/3204611/Georgia-2025-HB136-Enrolled.pdf#page=2", + "https://www.law.cornell.edu/uscode/text/26/24#c", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.states.ga.tax.income.credits.ctc + age = person("age", period) + # Georgia gives "qualifying child" the meaning of IRC 24(c) only, so + # it is not limited to children for whom the federal credit is + # allowed (IRC 24(a), 152(b)(1)). Whether a filer who can be claimed + # as a dependent can claim it is not settled; this keeps the IRC + # 24(c) child test. + p_federal = parameters(period).gov.irs.credits.ctc + ctc_eligible_child = ( + person("is_tax_unit_dependent", period) + & (age < p_federal.amount.base.thresholds[-1]) + & person("meets_ctc_child_identification_requirements", period) + ) + return ctc_eligible_child & (age < p.age_threshold) diff --git a/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py index 61195c8cb2c..52e40cca462 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_potential.py @@ -14,20 +14,6 @@ class ga_ctc_potential(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ga.tax.income.credits.ctc - person = tax_unit.members - age = person("age", period) - # Georgia gives "qualifying child" the meaning of IRC 24(c) only, so - # it is not limited to children for whom the federal credit is - # allowed (IRC 24(a), 152(b)(1)). Whether a filer who can be claimed - # as a dependent can claim it is not settled; this keeps the IRC - # 24(c) child test. - p_federal = parameters(period).gov.irs.credits.ctc - ctc_eligible_child = ( - person("is_tax_unit_dependent", period) - & (age < p_federal.amount.base.thresholds[-1]) - & person("meets_ctc_child_identification_requirements", period) - ) - ga_child_age_eligible = age < p.age_threshold - eligible_children = tax_unit.sum(ctc_eligible_child & ga_child_age_eligible) + eligible_children = add(tax_unit, period, ["ga_ctc_eligible_child"]) return eligible_children * p.amount From e4b8f36f08479f3bf47598ae0d3c7ecd1a06da84 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:01:52 -0400 Subject: [PATCH 13/32] Separate dependency facts and fix Hawaii qualified-person counts --- .../head-dependent-elsewhere-swap.fixed.md | 2 +- .../watca/working_americans_tax_cut_act.py | 13 +- .../repeal_state_dependent_exemptions.py | 2 +- .../reforms/states/ct/hb5114/ct_hb5114.py | 11 +- ...t_elsewhere_head_spouse_swap_invariance.py | 211 ++++++++++- .../income/medicaid_is_tax_dependent.yaml | 49 +++ .../eitc_demographic_eligible.yaml | 78 +++- .../savers_credit_eligible_person.yaml | 22 ++ .../az_aged_exemption_eligible_person.yaml | 38 +- .../food_excise/hi_food_excise_credit.yaml | 339 ++++++++++++++++++ .../tax/income/credits/hi_act_115_rebate.yaml | 63 ++++ ...w_income_household_renters_tax_credit.yaml | 65 ++++ .../tax/income/exemptions/mi_exemptions.yaml | 89 +++++ .../oh_personal_exemptions_eligible.yaml | 11 + ...nter_relief_pre_one_claimant_eligible.yaml | 21 ++ ...imable_as_dependent_on_another_return.yaml | 34 ++ ...nt_elsewhere_without_filing_exception.yaml | 36 ++ ...nt_elsewhere_without_filing_exception.yaml | 119 ++++++ .../tests/policy/contrib/congress/watca.yaml | 33 +- .../contrib/states/ct/hb5114/ct_hb5114.yaml | 2 +- .../eligibility/eitc_demographic_eligible.py | 8 +- .../az_aged_exemption_eligible_person.py | 10 +- .../food_excise/hi_food_excise_credit.py | 5 +- ...hi_food_excise_credit_minor_child_count.py | 8 +- .../hi_food_excise_exemption_amount.py | 31 +- .../tax/income/credits/hi_act_115_rebate.py | 20 +- ...low_income_household_renters_tax_credit.py | 21 +- .../credits/me_affordability_payment.py | 4 +- .../mi/tax/income/exemptions/mi_exemptions.py | 15 +- ...omestead_credit_refund_household_income.py | 2 +- .../credits/nm_medical_expense_credit.py | 2 +- .../oh_personal_exemptions_eligible_person.py | 2 +- ...renter_relief_pre_one_claimant_eligible.py | 9 +- ...laimable_as_dependent_on_another_return.py | 23 ++ .../claimed_as_dependent_on_another_return.py | 11 +- .../dependent_claimant_filing_exception.py | 21 ++ ...dent_elsewhere_without_filing_exception.py | 22 ++ .../every_filer_is_dependent_elsewhere.py | 4 +- .../tax_unit/head_is_dependent_elsewhere.py | 8 +- .../head_or_spouse_is_dependent_elsewhere.py | 10 +- ...dent_elsewhere_without_filing_exception.py | 23 ++ ...ad_spouse_count_not_dependent_elsewhere.py | 8 +- .../tax_unit/spouse_is_dependent_elsewhere.py | 8 +- 43 files changed, 1399 insertions(+), 114 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_is_tax_dependent.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/household/demographic/person/claimable_as_dependent_on_another_return.yaml create mode 100644 policyengine_us/tests/policy/baseline/household/demographic/person/is_dependent_elsewhere_without_filing_exception.yaml create mode 100644 policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.yaml create mode 100644 policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py create mode 100644 policyengine_us/variables/household/demographic/person/dependent_claimant_filing_exception.py create mode 100644 policyengine_us/variables/household/demographic/person/is_dependent_elsewhere_without_filing_exception.py create mode 100644 policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.py diff --git a/changelog.d/head-dependent-elsewhere-swap.fixed.md b/changelog.d/head-dependent-elsewhere-swap.fixed.md index 42a32945776..5222c01b901 100644 --- a/changelog.d/head-dependent-elsewhere-swap.fixed.md +++ b/changelog.d/head-dependent-elsewhere-swap.fixed.md @@ -1 +1 @@ -The federal standard deduction and the New York, Michigan, Oregon, Virginia, Missouri, New Mexico, Maine and Hawaii rules for a filer claimed as a dependent elsewhere no longer depend on which spouse is labelled head; each follows its jurisdiction's rule for a joint return where one spouse can be claimed. New York's $3,100 dependent standard deduction now applies only to single filers; Hawaii now limits the standard deduction of a filer who can be claimed as a dependent; the federal EITC without a qualifying child now requires that no filer can be claimed as a dependent; and New Mexico's medical care credit requires a filer who is both 65 or older and not a dependent when a filer is a dependent elsewhere. +The federal standard deduction and the New York, Michigan, Oregon, Virginia, Missouri, New Mexico, Maine and Hawaii rules for a filer claimed as a dependent elsewhere no longer depend on which spouse is labelled head; each follows its jurisdiction's rule for a joint return where one spouse can be claimed. New York's $3,100 dependent standard deduction now applies only to single filers; Hawaii now limits the standard deduction of a filer who can be claimed as a dependent; the federal EITC without a qualifying child now requires that no filer can be claimed as a dependent; and New Mexico's medical care credit requires a filer who is both 65 or older and not a dependent when a filer is a dependent elsewhere. Separate actual dependency claims from claimability and the nonrequired claimant filing exception, preserving dependent deduction limits while restoring EITC and lawful own-dependent counts. Exclude externally claimable children from Hawaii food/excise ordinary exemptions. diff --git a/policyengine_us/reforms/congress/watca/working_americans_tax_cut_act.py b/policyengine_us/reforms/congress/watca/working_americans_tax_cut_act.py index 860462a6ed5..27becb3a73f 100644 --- a/policyengine_us/reforms/congress/watca/working_americans_tax_cut_act.py +++ b/policyengine_us/reforms/congress/watca/working_americans_tax_cut_act.py @@ -52,7 +52,11 @@ class watca_alternative_tax_eligible(Variable): label = "Eligible for WATCA alternative maximum tax" definition_period = YEAR reference = WATCA_REFERENCES - documentation = "Section 1A(b): MAGI < 175% of exemption, and not claimed as dependent on another return." + documentation = ( + "Section 1A(b): MAGI < 175% of exemption. Retains the reform's " + "modeled actual-dependent-claim restriction, which is not stated " + "in the cited bill's section 1A(b)(2)." + ) def formula(tax_unit, period, parameters): magi = tax_unit("watca_alternative_tax_magi", period) @@ -61,9 +65,10 @@ def formula(tax_unit, period, parameters): exemption_amount = p.amount[filing_status] income_limit = exemption_amount * p.income_limit_multiple income_eligible = magi < income_limit - head_dependent = tax_unit("head_is_dependent_elsewhere", period) - spouse_dependent = tax_unit("spouse_is_dependent_elsewhere", period) - not_dependent = ~head_dependent & ~spouse_dependent + person = tax_unit.members + claimed = person("claimed_as_dependent_on_another_return", period) + filer = person("is_tax_unit_head_or_spouse", period) + not_dependent = ~tax_unit.any(claimed & filer) return income_eligible & not_dependent class watca_alternative_max_tax(Variable): diff --git a/policyengine_us/reforms/state_dependent_exemptions/repeal_state_dependent_exemptions.py b/policyengine_us/reforms/state_dependent_exemptions/repeal_state_dependent_exemptions.py index 55765f89e4b..d05820bfb6b 100644 --- a/policyengine_us/reforms/state_dependent_exemptions/repeal_state_dependent_exemptions.py +++ b/policyengine_us/reforms/state_dependent_exemptions/repeal_state_dependent_exemptions.py @@ -93,7 +93,7 @@ def formula(person, period, parameters): # if they are not claimed as a dependent elsewhere head_or_spouse = person("is_tax_unit_head_or_spouse", period) dependent_on_another_return = person( - "claimed_as_dependent_on_another_return", period + "claimable_as_dependent_on_another_return", period ) # The personal exemption is also provided to dependents return ~dependent_on_another_return & head_or_spouse diff --git a/policyengine_us/reforms/states/ct/hb5114/ct_hb5114.py b/policyengine_us/reforms/states/ct/hb5114/ct_hb5114.py index 91d44eb6f65..c95f0f8a032 100644 --- a/policyengine_us/reforms/states/ct/hb5114/ct_hb5114.py +++ b/policyengine_us/reforms/states/ct/hb5114/ct_hb5114.py @@ -19,11 +19,12 @@ def formula(tax_unit, period, parameters): rent = add(tax_unit, period, ["rent"]) pays_rent = rent > 0 - # Must not be claimed as a dependent on another return - head_is_dependent_elsewhere = tax_unit( - "head_is_dependent_elsewhere", period - ) - not_dependent = ~head_is_dependent_elsewhere + # Preserve the modeled actual-claim gate; the cited bill does + # not state this restriction. + person = tax_unit.members + claimed = person("claimed_as_dependent_on_another_return", period) + head = person("is_tax_unit_head", period) + not_dependent = ~tax_unit.any(claimed & head) # Income eligibility filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py index 4e681240eb5..4d5f97ae469 100644 --- a/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py +++ b/policyengine_us/tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py @@ -13,7 +13,8 @@ - Either spouse limits the return (the IRC 63(c)(5) worksheet): the federal standard deduction, Oregon's and Hawaii's standard deductions, and Virginia's credit for low-income individuals. The federal EITC without a - qualifying child now also requires that neither spouse can be claimed. + qualifying child also requires that neither spouse is claimable outside the + nonrequired-claimant filing exception. - Only single filers: New York's dependent standard deduction. - Each spouse separately: Michigan's exemptions and Hawaii's food/excise, renters and Act 115 credits count only the filers who cannot be claimed. @@ -26,7 +27,8 @@ Hypothesis draws batches of married couples, with and without dependents, in the eight states with such a rule and one without, for 2021-2026, with either, -both or neither spouse claimed elsewhere, and checks 1 and 3. A seeded +both or neither spouse claimed or claimable elsewhere, independently of the +nonrequired-claimant filing exception, and checks 1 and 3. A seeded population and crafted cases check 1, 2 and 3 in every year, and two seeded couples plus the review case in every state check 1 for federal and state income tax in 2026. Each batch is one vectorized simulation. For each couple: @@ -41,8 +43,20 @@ 3. The helper identities: `head_or_spouse_is_dependent_elsewhere` equals `head_is_dependent_elsewhere | spouse_is_dependent_elsewhere`, `every_filer_is_dependent_elsewhere` equals their conjunction for a - couple, and `head_spouse_count_not_dependent_elsewhere` plus the claimed + couple, and `head_spouse_count_not_dependent_elsewhere` plus the claimable filers equals the number of filers. + +The 2025 seeded population also compares legacy input only with explicit +claimability equal to that input, protecting the new input's fallback. A +separate vectorized population holds actual claims and claimability fixed +while toggling the exception: general claimability rules and Medicaid stay +unchanged, childless EITC eligibility is restored, and Michigan/Hawaii may +count the filer's own dependents. Benefits derived from the restored federal +EITC may also change: Missouri's working family credit eligibility must track +positive federal EITC for this population's joint returns with no investment +income. Its separate claimability gate stays fixed. YAML examples establish +the corresponding legal amounts, including the limited $2,450 federal +standard deduction. """ import numpy as np @@ -62,10 +76,11 @@ "state_income_tax", "state_income_tax_before_refundable_credits", ] -# The formulas that read whether a filer is claimed elsewhere. +# The formulas that read a filer's claimability or dependent-taxpayer status. CONSUMERS = [ "basic_standard_deduction", "eitc", + "eitc_demographic_eligible", "ny_standard_deduction", "mi_exemptions", "or_standard_deduction", @@ -81,11 +96,32 @@ "hi_deductions", "hi_act_115_rebate", "hi_food_excise_exemption_amount", + "hi_food_excise_credit_minor_child_count", "hi_food_excise_credit", "hi_tax_credit_for_low_income_household_renters_eligible", "hi_tax_credit_for_low_income_household_renters", ] CLAIM_PATTERNS = [(True, False), (False, True), (True, True), (False, False)] +DEPENDENT_TAXPAYER_TEST_OUTPUTS = { + "mi_exemptions", + "hi_act_115_rebate", + "hi_food_excise_exemption_amount", + "hi_food_excise_credit", + "hi_tax_credit_for_low_income_household_renters", +} +# Federal EITC eligibility is restored directly. The federal amount and +# Missouri eligibility can change as a consequence, without changing their +# general claimability gates. +EITC_RELATED_OUTPUTS = { + "eitc_demographic_eligible", + "eitc", + "mo_wftc_eligible", +} +EXCEPTION_SENSITIVE = DEPENDENT_TAXPAYER_TEST_OUTPUTS | EITC_RELATED_OUTPUTS +ACTUAL_CLAIM_OUTPUTS = [ + "medicaid_is_tax_dependent", + "medicaid_uses_missing_claimant_fallback", +] def money(high): @@ -123,11 +159,17 @@ def dependents(draw): @st.composite def couples(draw): + claimable = draw(st.sampled_from(CLAIM_PATTERNS)) + claimed = tuple(can_claim and draw(st.booleans()) for can_claim in claimable) return { "state": draw(st.sampled_from(AFFECTED)), "adults": [draw(adults()), draw(adults())], "dependents": draw(st.lists(dependents(), max_size=2)), - "claimed": draw(st.sampled_from(CLAIM_PATTERNS)), + "claimed": claimed, + "claimable": claimable, + "exception": tuple( + not is_claimed and draw(st.booleans()) for is_claimed in claimed + ), } @@ -214,7 +256,7 @@ def _crafted_cases(states, review_case_only=False): return cases -def _situation(units, year, variants): +def _situation(units, year, variants, explicit_claimability=False): # Each unit appears once per variant: (head is the first adult, first # adult claimed, second adult claimed). People keep their order and every # other input in every copy. @@ -227,11 +269,20 @@ def _situation(units, year, variants): "households": {}, } for i, unit in enumerate(units): - for copy, (first_is_head, claim_a, claim_b) in enumerate(variants(unit)): + for copy, variant in enumerate(variants(unit)): + first_is_head, claim_a, claim_b = variant[:3] + claimable = unit.get("claimable") + exception = unit.get("exception") + if len(variant) == 5: + claimable, exception = variant[3:] + if explicit_claimability: + claimable = (claim_a, claim_b) a, b = f"a_{i}_{copy}", f"b_{i}_{copy}" - for name, amounts, is_head, claimed in ( - (a, unit["adults"][0], first_is_head, claim_a), - (b, unit["adults"][1], not first_is_head, claim_b), + for adult_index, (name, amounts, is_head, claimed) in enumerate( + ( + (a, unit["adults"][0], first_is_head, claim_a), + (b, unit["adults"][1], not first_is_head, claim_b), + ) ): people[name] = { **amounts, @@ -240,6 +291,14 @@ def _situation(units, year, variants): "is_tax_unit_dependent": False, "claimed_as_dependent_on_another_return": claimed, } + if claimable is not None: + people[name]["claimable_as_dependent_on_another_return"] = ( + claimable[adult_index] + ) + if exception is not None: + people[name]["dependent_claimant_filing_exception"] = exception[ + adult_index + ] members = [a, b] groups["marital_units"][f"couple_{i}_{copy}"] = {"members": [a, b]} for j, amounts in enumerate(unit["dependents"]): @@ -250,6 +309,10 @@ def _situation(units, year, variants): "is_tax_unit_spouse": False, "is_tax_unit_dependent": True, } + if explicit_claimability: + people[child]["claimable_as_dependent_on_another_return"] = ( + amounts.get("claimed_as_dependent_on_another_return", False) + ) members.append(child) groups["marital_units"][f"single_{i}_{copy}_{j}"] = {"members": [child]} for group, prefix in ( @@ -278,7 +341,7 @@ def _calc(sim, name, year): return np.asarray(sim.calculate(name, year), dtype=float) -def _check_swap(units, year, outputs): +def _check_swap(units, year, outputs, capture_fallback=False): sim = Simulation(situation=_situation(units, year, _swap_variants)) states = np.array([u["state"] for u in units]) for name in outputs: @@ -304,6 +367,125 @@ def _check_swap(units, year, outputs): np.testing.assert_array_equal(independent + head + spouse, 2) every = _calc(sim, "every_filer_is_dependent_elsewhere", year) np.testing.assert_array_equal(every, np.minimum(head, spouse)) + blocked = _calc(sim, "is_dependent_elsewhere_without_filing_exception", year) + filers = _calc(sim, "is_tax_unit_head", year) + _calc( + sim, "is_tax_unit_spouse", year + ) + np.testing.assert_array_equal( + _calc( + sim, "head_or_spouse_is_dependent_elsewhere_without_filing_exception", year + ), + sim.populations["tax_unit"].any(blocked.astype(bool) & filers.astype(bool)), + ) + if capture_fallback: + # Keep only arrays so the compatibility comparison can release this + # simulation before constructing the next full model. + return { + name: _calc(sim, name, year) + for name in TAX_UNIT_OUTPUTS + CONSUMERS + ACTUAL_CLAIM_OUTPUTS + } + + +def _check_fallback(units, year, legacy_outputs): + # The absence of a new input is simulation-wide: compare separate + # simulations so Core does not fill omitted people with input defaults. + explicit_sim = Simulation( + situation=_situation(units, year, _swap_variants, explicit_claimability=True) + ) + for name in TAX_UNIT_OUTPUTS + CONSUMERS + ACTUAL_CLAIM_OUTPUTS: + np.testing.assert_allclose( + legacy_outputs[name], + _calc(explicit_sim, name, year), + atol=TOLERANCE, + rtol=0, + err_msg=f"{name} changes when fallback claimability is made explicit", + ) + + +def _exception_variants(unit): + claimable = unit["claimed"] + return [ + (True, False, False, claimable, (False, False)), + (False, False, False, claimable, (False, False)), + (True, False, False, claimable, (True, True)), + (False, False, False, claimable, (True, True)), + ] + + +def _check_exception_scope(): + year = 2025 + units = [ + { + "state": state, + "adults": [{"age": 26, "employment_income": 2_000}, {"age": 30}], + "dependents": children, + "claimed": claimed, + } + for state in AFFECTED + # Isolate ordinary dependents: publicly supported minors have a + # separate Hawaii qualification that does not use this exception. + for children in ( + [], + [{"age": 5, "hi_food_excise_credit_child_receiving_public_support": False}], + ) + for claimed in CLAIM_PATTERNS + ] + sim = Simulation(situation=_situation(units, year, _exception_variants)) + for name in CONSUMERS + ACTUAL_CLAIM_OUTPUTS: + values = _calc(sim, name, year) + if name in ACTUAL_CLAIM_OUTPUTS: + # Person outputs have two filers and optionally one child per unit. + start = 0 + for unit in units: + size = 2 + len(unit["dependents"]) + copies = values[start : start + 4 * size].reshape(4, size) + np.testing.assert_array_equal(copies[0:2], copies[2:4]) + start += 4 * size + continue + copies = values.reshape(-1, 4) + np.testing.assert_allclose( + copies[:, 0::2], copies[:, 1::2], atol=TOLERANCE, rtol=0 + ) + if name not in EXCEPTION_SENSITIVE: + np.testing.assert_allclose( + copies[:, :2], + copies[:, 2:], + atol=TOLERANCE, + rtol=0, + err_msg=f"The filing exception changes general claimability rule {name}", + ) + eligibility = _calc(sim, "eitc_demographic_eligible", year).reshape(-1, 4) + childless = np.array([not unit["dependents"] for unit in units]) + claimable = np.array([any(unit["claimed"]) for unit in units]) + assert not eligibility[childless & claimable, :2].any() + assert eligibility[childless, 2:].all() + # RSMo 143.177.2 requires federal EITC. These units file jointly, so the + # separate dependent-filer stop does not apply, and their zero investment + # income meets Missouri's limit. The exception's Missouri effect must + # therefore follow positive federal EITC, rather than alter claimability. + missouri = np.array([unit["state"] == "MO" for unit in units]) + federal_eitc = _calc(sim, "eitc", year).reshape(-1, 4) + missouri_eligible = _calc(sim, "mo_wftc_eligible", year).reshape(-1, 4) + np.testing.assert_array_equal( + missouri_eligible[missouri].astype(bool), federal_eitc[missouri] > 0 + ) + direct_claimability = _calc( + sim, "head_or_spouse_is_dependent_elsewhere", year + ).reshape(-1, 4) + np.testing.assert_array_equal( + direct_claimability, np.repeat(claimable[:, None], 4, axis=1) + ) + for name, state in (("mi_exemptions", "MI"), ("hi_food_excise_credit", "HI")): + values = _calc(sim, name, year).reshape(-1, 4) + restores_child = np.array( + [ + unit["state"] == state + and bool(unit["dependents"]) + and sum(unit["claimed"]) == 1 + for unit in units + ] + ) + assert (values[restores_child, 2:] > values[restores_child, :2]).all() def _monotone_variants(unit): @@ -355,7 +537,12 @@ def test_claimed_spouse_does_not_depend_on_head_label(year, units): @pytest.mark.parametrize("year", YEARS) def test_seeded_population(year): units = _seeded_couples(AFFECTED, per_state=2) + _crafted_cases(AFFECTED) - _check_swap(units, year, TAX_UNIT_OUTPUTS + CONSUMERS) + legacy_outputs = _check_swap( + units, year, TAX_UNIT_OUTPUTS + CONSUMERS, capture_fallback=year == 2025 + ) + if year == 2025: + _check_fallback(units, year, legacy_outputs) + _check_exception_scope() _check_monotone(units, year) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_is_tax_dependent.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_is_tax_dependent.yaml new file mode 100644 index 00000000000..c04ad8732f3 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_is_tax_dependent.yaml @@ -0,0 +1,49 @@ +- name: Claimability alone does not create an expected Medicaid tax-dependent claim + period: 2025 + input: + age: 18 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + medicaid_has_known_claiming_tax_unit: false + tax_unit_is_filer: true + output: + # 42 CFR 435.603(f)(1)-(2) uses expected actual claims, not claimability. + medicaid_is_tax_dependent: false + medicaid_uses_missing_claimant_fallback: false + medicaid_uses_non_filer_rules: false + +- name: An expected actual claim invokes Medicaid's dependent and missing-claimant rules + period: 2025 + input: + age: 18 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true + medicaid_has_known_claiming_tax_unit: false + tax_unit_is_filer: true + output: + medicaid_is_tax_dependent: true + medicaid_uses_missing_claimant_fallback: true + medicaid_uses_non_filer_rules: true + +- name: The tax filing exception leaves Medicaid's expected actual claim unchanged + period: 2025 + input: + age: 18 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + medicaid_has_known_claiming_tax_unit: false + tax_unit_is_filer: true + output: + medicaid_is_tax_dependent: true + medicaid_uses_missing_claimant_fallback: true + medicaid_uses_non_filer_rules: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml index b922b61cb47..584e066b887 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible.yaml @@ -258,7 +258,7 @@ # qualifying child. eitc_demographic_eligible: true -- name: 2025 childless single filer whose parent files only to claim a refund is not a dependent +- name: 2025 claimable childless filer qualifies when nonrequired parent files only for a withholding refund period: 2025 input: people: @@ -266,6 +266,8 @@ age: 26 employment_income: 2_000 claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true tax_units: tax_unit: members: [head] @@ -274,7 +276,75 @@ members: [head] state_code: CA output: - # Publication 596 (2025, p.18): the credit is allowed when the person who can claim - # you files only to claim a refund; such a person is not a dependent, so the - # input is false. + # Pub. 596 PDF p.18: nonrequired parent files only for a refund of income tax + # withheld. Pub. 501 PDF p.24: claimability still limits the standard deduction. eitc_demographic_eligible: true + basic_standard_deduction: 2_450 + +- name: 2025 claimable childless filer qualifies when nonrequired parent files no return + period: 2025 + input: + people: + head: + age: 26 + employment_income: 2_000 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: CA + output: + # Pub. 596 PDF p.18 also covers a nonrequired claimant who files no return. + # The exception changes EITC, while Pub. 501 PDF p.24 still limits deductions. + eitc_demographic_eligible: true + basic_standard_deduction: 2_450 + +- name: 2025 unclaimed childless filer remains ineligible when claimable by a parent required to file + period: 2025 + input: + people: + head: + age: 26 + employment_income: 2_000 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: false + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: CA + output: + # Pub. 596 PDF p.18 Example 2: choosing not to claim the filer is insufficient. + eitc_demographic_eligible: false + basic_standard_deduction: 2_450 + +- name: 2025 unclaimed childless filer remains ineligible when parent files for refundable credits + period: 2025 + input: + people: + head: + age: 26 + employment_income: 2_000 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: false + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: CA + output: + # Pub. 596 PDF pp.18-19: filing for a credit is not filing solely for a refund + # of withholding or estimated payments, even if no return was required. + eitc_demographic_eligible: false + basic_standard_deduction: 2_450 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_eligible_person.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_eligible_person.yaml index 7309a5c6dc6..c070b58f18f 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_eligible_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_eligible_person.yaml @@ -17,3 +17,25 @@ is_full_time_student: false output: savers_credit_eligible_person: false + +- name: Savers credit admits a claimable filer who is not actually claimed + period: 2025 + input: + age: 26 + is_full_time_student: false + is_tax_unit_head_or_spouse: true + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + output: + savers_credit_eligible_person: true + +- name: Savers credit excludes an actually claimed filer + period: 2025 + input: + age: 26 + is_full_time_student: false + is_tax_unit_head_or_spouse: true + claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true + output: + savers_credit_eligible_person: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.yaml index a4d945380ab..1bb415b0fed 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.yaml @@ -4,13 +4,13 @@ people: person1: is_tax_unit_head: true + claimed_as_dependent_on_another_return: false person2: is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true tax_units: tax_unit: az_filing_status: JOINT - head_is_dependent_elsewhere: false - spouse_is_dependent_elsewhere: true members: [person1, person2] household: members: [person1, person2] @@ -24,13 +24,13 @@ people: person1: is_tax_unit_head: true + claimed_as_dependent_on_another_return: true person2: is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true tax_units: tax_unit: az_filing_status: JOINT - head_is_dependent_elsewhere: true - spouse_is_dependent_elsewhere: true members: [person1, person2] household: members: [person1, person2] @@ -44,13 +44,13 @@ people: person1: is_tax_unit_head: true + claimed_as_dependent_on_another_return: false person2: is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false tax_units: tax_unit: az_filing_status: SEPARATE - head_is_dependent_elsewhere: false - spouse_is_dependent_elsewhere: false members: [person1, person2] household: members: [person1, person2] @@ -64,19 +64,41 @@ people: person1: is_tax_unit_head: true + claimed_as_dependent_on_another_return: false person2: is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false person3: is_tax_unit_head: false is_tax_unit_spouse: false tax_units: tax_unit: az_filing_status: SEPARATE - head_is_dependent_elsewhere: false - spouse_is_dependent_elsewhere: false members: [person1, person2, person3] household: members: [person1, person2, person3] state_code: AZ output: az_aged_exemption_eligible_person: [true, false, false] + +- name: Arizona aged exemption retains an unclaimed claimable filer + period: 2025 + input: + state_code: AZ + age: 70 + is_tax_unit_head: true + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + output: + az_aged_exemption_eligible_person: true + +- name: Arizona aged exemption excludes an actually claimed filer + period: 2025 + input: + state_code: AZ + age: 70 + is_tax_unit_head: true + claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true + output: + az_aged_exemption_eligible_person: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml index 260651381eb..c4daec08bc0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.yaml @@ -595,3 +595,342 @@ output: # Only the spouse who cannot be claimed counts: 1 x $220. hi_food_excise_credit: 220 + +# N-311 (2022 and 2025), PDF p.1 lines 2-3, excludes externally claimable +# persons from both ordinary qualified persons and publicly supported minors. +# At $4,000 AGI, 2022 lines 7-10 give 2 x $110; 2025 lines 7-9 give 2 x $220. +- name: 2022 independent joint filers exclude externally claimable publicly supported child when person_a is head + period: 2022 + input: + people: + person_a: + age: 40 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: false + dependent_claimant_filing_exception: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 45 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_food_excise_credit: 220 + hi_food_excise_exemption_amount: 220 + hi_food_excise_credit_minor_child_count: 0 + +- name: 2022 independent joint filers exclude externally claimable publicly supported child when person_b is head + period: 2022 + input: + people: + person_b: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 40 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: false + dependent_claimant_filing_exception: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + hi_food_excise_credit: 220 + hi_food_excise_exemption_amount: 220 + hi_food_excise_credit_minor_child_count: 0 + +- name: 2025 independent joint filers exclude externally claimable publicly supported child when person_a is head + period: 2025 + input: + people: + person_a: + age: 40 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: false + dependent_claimant_filing_exception: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 45 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_food_excise_credit: 440 + hi_food_excise_exemption_amount: 440 + hi_food_excise_credit_minor_child_count: 0 + +- name: 2025 independent joint filers exclude externally claimable publicly supported child when person_b is head + period: 2025 + input: + people: + person_b: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 40 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: false + dependent_claimant_filing_exception: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + hi_food_excise_credit: 440 + hi_food_excise_exemption_amount: 440 + hi_food_excise_credit_minor_child_count: 0 + +- name: 2022 joint return filing exception restores own ordinary dependent but excludes claimable head + period: 2022 + input: + people: + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_food_excise_credit_minor_child_count: 0 + hi_food_excise_credit: 220 + hi_food_excise_exemption_amount: 220 + +- name: 2022 joint return filing exception restores own ordinary dependent but excludes claimable spouse + period: 2022 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + hi_food_excise_credit_minor_child_count: 0 + hi_food_excise_credit: 220 + hi_food_excise_exemption_amount: 220 + +- name: 2025 joint return filing exception restores own ordinary dependent but excludes claimable head + period: 2025 + input: + people: + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_food_excise_credit_minor_child_count: 0 + hi_food_excise_credit: 440 + hi_food_excise_exemption_amount: 440 + +- name: 2025 joint return filing exception restores own ordinary dependent but excludes claimable spouse + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + hi_food_excise_credit_minor_child_count: 0 + hi_food_excise_credit: 440 + hi_food_excise_exemption_amount: 440 + +# A publicly supported minor filer belongs to N-311 line 2, not line 3. +# At $16,000 AGI in 2022, 2 ordinary persons x $70 = $140. +- name: 2022 publicly supported minor filer is only an ordinary qualified person when labelled head + period: 2022 + input: + people: + person_a: + age: 17 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: false + dependent_claimant_filing_exception: false + is_tax_unit_head: true + is_tax_unit_spouse: false + hi_food_excise_credit_child_receiving_public_support: true + person_b: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + adjusted_gross_income: 16000 + households: + household: + members: [person_a, person_b] + state_code: HI + output: + hi_food_excise_credit: 140 + hi_food_excise_credit_minor_child_count: 0 + +- name: 2022 publicly supported minor filer is only an ordinary qualified person when labelled spouse + period: 2022 + input: + people: + person_b: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 17 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: false + dependent_claimant_filing_exception: false + is_tax_unit_head: false + is_tax_unit_spouse: true + hi_food_excise_credit_child_receiving_public_support: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + adjusted_gross_income: 16000 + households: + household: + members: [person_b, person_a] + state_code: HI + output: + hi_food_excise_credit: 140 + hi_food_excise_credit_minor_child_count: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml index 7c4030e8e4c..124ffe32e48 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_act_115_rebate.yaml @@ -252,3 +252,66 @@ state_code: HI output: hi_act_115_rebate: 0 + +# Act 115 sec.3(a)-(b), HRS 235-54(a), and Pub. 501 PDF p.11. +- name: Joint return filing exception restores own dependent but excludes claimable head from Act 115 + period: 2021 + input: + people: + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_act_115_rebate: 600 + +- name: Joint return filing exception restores own dependent but excludes claimable spouse from Act 115 + period: 2021 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + hi_act_115_rebate: 600 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml index 2dd4cd51e42..25fa5e14ce5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml @@ -101,3 +101,68 @@ # Schedule X lines 8-10: the spouse who cannot be claimed, plus their age # 65 exemption. 2 x $50. hi_tax_credit_for_low_income_household_renters: 100 + +# Schedule X lines 8-10; Pub. 501 filing exception restores only dependents. +- name: Joint return filing exception restores own dependent without claimable head personal or aged exemption + period: 2021 + input: + people: + person_a: + age: 66 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 66 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_tax_credit_for_low_income_household_renters: 150 + +- name: Joint return filing exception restores own dependent without claimable spouse personal or aged exemption + period: 2021 + input: + people: + person_b: + age: 66 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 66 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + hi_tax_credit_for_low_income_household_renters: 150 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml index eee9da8f35d..e67f331c532 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/exemptions/mi_exemptions.yaml @@ -445,3 +445,92 @@ # A joint return on which a spouse can be claimed claims no dependents # (IRS Publication 501), so the child adds no exemption. mi_exemptions: 7_300 + +# Pub. 501 (2025), Dependent Taxpayer Test, PDF p.11; MCL 206.30(4). +- name: 2025 joint return filing exception restores own dependent while claimable head keeps the dependent allowance + period: 2025 + input: + people: + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_a, person_b, child_1] + state_code: MI + output: + mi_exemptions: 13100 + +- name: 2025 joint return filing exception restores own dependent while claimable spouse keeps the dependent allowance + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 26 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + households: + household: + members: [person_b, person_a, child_1] + state_code: MI + output: + mi_exemptions: 13100 + +- name: 2025 single filer with a nonrequired claimant who files no return may claim their own dependent + period: 2025 + input: + people: + filer: + age: 26 + is_tax_unit_head: true + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [filer, child] + filing_status: SINGLE + households: + household: + members: [filer, child] + state_code: MI + output: + # $1,500 claimable-filer allowance + $5,800 for their child. + mi_exemptions: 7_300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml index 97f70fe2376..d9fe76117ae 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/exemptions/personal/oh_personal_exemptions_eligible.yaml @@ -27,3 +27,14 @@ is_tax_unit_dependent: true output: oh_personal_exemptions_eligible_person: true + +- name: Ohio personal exemption excludes a claimable but unclaimed filer + period: 2025 + input: + state_code: OH + is_tax_unit_head_or_spouse: true + is_tax_unit_dependent: false + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + output: + oh_personal_exemptions_eligible_person: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.yaml new file mode 100644 index 00000000000..64ee21b5111 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.yaml @@ -0,0 +1,21 @@ +- name: Utah property relief retains an unclaimed claimable filer + period: 2025 + input: + state_code: UT + age: 66 + rent: 12_000 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + output: + ut_homeowner_renter_relief_pre_one_claimant_eligible: true + +- name: Utah property relief excludes an actually claimed filer + period: 2025 + input: + state_code: UT + age: 66 + rent: 12_000 + claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true + output: + ut_homeowner_renter_relief_pre_one_claimant_eligible: false diff --git a/policyengine_us/tests/policy/baseline/household/demographic/person/claimable_as_dependent_on_another_return.yaml b/policyengine_us/tests/policy/baseline/household/demographic/person/claimable_as_dependent_on_another_return.yaml new file mode 100644 index 00000000000..2530e578832 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/demographic/person/claimable_as_dependent_on_another_return.yaml @@ -0,0 +1,34 @@ +- name: An existing actual claim supplies the default claimability + period: 2025 + input: + claimed_as_dependent_on_another_return: true + output: + claimable_as_dependent_on_another_return: true + +- name: An existing false claim input preserves default independence + period: 2025 + input: + claimed_as_dependent_on_another_return: false + output: + claimable_as_dependent_on_another_return: false + +- name: No claim inputs preserves default independence + period: 2025 + input: + age: 26 + output: + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: false + +- name: A parent can claim the filer without making or expecting a claim + period: 2025 + input: + age: 26 + employment_income: 2_000 + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + output: + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + basic_standard_deduction: 2_450 + eitc_demographic_eligible: false diff --git a/policyengine_us/tests/policy/baseline/household/demographic/person/is_dependent_elsewhere_without_filing_exception.yaml b/policyengine_us/tests/policy/baseline/household/demographic/person/is_dependent_elsewhere_without_filing_exception.yaml new file mode 100644 index 00000000000..4164a45064a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/demographic/person/is_dependent_elsewhere_without_filing_exception.yaml @@ -0,0 +1,36 @@ +- name: Unclaimed but claimable filer fails the dependent taxpayer test + period: 2025 + input: + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + output: + dependent_claimant_filing_exception: false + is_dependent_elsewhere_without_filing_exception: true + +- name: Nonrequired refund-only claimant restores dependent taxpayer eligibility + period: 2025 + input: + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + output: + # Publication 501 PDF p.11, Dependent Taxpayer Test exception. + is_dependent_elsewhere_without_filing_exception: false + claimable_as_dependent_on_another_return: true + +- name: Nonrequired claimant filing no return restores dependent taxpayer eligibility + period: 2025 + input: + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + output: + # Publication 596 PDF p.18 includes the nonrequired claimant who files no return. + is_dependent_elsewhere_without_filing_exception: false + claimable_as_dependent_on_another_return: true + +- name: The filing exception does not create claimability + period: 2025 + input: + claimable_as_dependent_on_another_return: false + dependent_claimant_filing_exception: true + output: + is_dependent_elsewhere_without_filing_exception: false diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.yaml new file mode 100644 index 00000000000..8b9e439d173 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.yaml @@ -0,0 +1,119 @@ +- name: Claimable unclaimed head prevents claiming the return's own dependents + period: 2025 + input: + people: + person_a: + is_tax_unit_head: true + is_tax_unit_spouse: false + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + person_b: + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [person_a, person_b] + output: + head_or_spouse_is_dependent_elsewhere: true + head_or_spouse_is_dependent_elsewhere_without_filing_exception: true + +- name: Claimable unclaimed spouse prevents claiming the return's own dependents + period: 2025 + input: + people: + person_b: + is_tax_unit_head: true + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: false + person_a: + is_tax_unit_head: false + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: false + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_b, person_a] + output: + head_or_spouse_is_dependent_elsewhere: true + head_or_spouse_is_dependent_elsewhere_without_filing_exception: true + +- name: The head's nonrequired claimant filing exception restores own-dependent eligibility + period: 2025 + input: + people: + person_a: + is_tax_unit_head: true + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + person_b: + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: false + tax_units: + tax_unit: + members: [person_a, person_b] + output: + head_or_spouse_is_dependent_elsewhere: true + head_or_spouse_is_dependent_elsewhere_without_filing_exception: false + +- name: The spouse's nonrequired claimant filing exception restores own-dependent eligibility + period: 2025 + input: + people: + person_b: + is_tax_unit_head: true + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: false + person_a: + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + tax_units: + tax_unit: + members: [person_b, person_a] + output: + head_or_spouse_is_dependent_elsewhere: true + head_or_spouse_is_dependent_elsewhere_without_filing_exception: false + +- name: One exception is insufficient when the other spouse has an ordinary claimant + period: 2025 + input: + people: + head: + is_tax_unit_head: true + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + spouse: + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: false + tax_units: + tax_unit: + members: [head, spouse] + output: + head_or_spouse_is_dependent_elsewhere_without_filing_exception: true + +- name: A claimable child does not apply the dependent taxpayer test to independent filers + period: 2025 + input: + people: + head: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: false + child: + age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, child] + output: + head_or_spouse_is_dependent_elsewhere_without_filing_exception: false diff --git a/policyengine_us/tests/policy/contrib/congress/watca.yaml b/policyengine_us/tests/policy/contrib/congress/watca.yaml index 500ed7e863b..2b40103ff7f 100644 --- a/policyengine_us/tests/policy/contrib/congress/watca.yaml +++ b/policyengine_us/tests/policy/contrib/congress/watca.yaml @@ -440,7 +440,7 @@ gov.contrib.congress.watca.in_effect: true adjusted_gross_income: 30_000 filing_status: SINGLE - head_is_dependent_elsewhere: true + claimed_as_dependent_on_another_return: true output: # Even though income is below limit, not eligible because claimed as dependent watca_alternative_tax_eligible: false @@ -450,9 +450,18 @@ reforms: policyengine_us.reforms.congress.watca.working_americans_tax_cut_act.watca_reform_object input: gov.contrib.congress.watca.in_effect: true - adjusted_gross_income: 30_000 - filing_status: JOINT - spouse_is_dependent_elsewhere: true + people: + head: + is_tax_unit_head: true + claimed_as_dependent_on_another_return: false + spouse: + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 30_000 + filing_status: JOINT output: watca_alternative_tax_eligible: false @@ -529,10 +538,18 @@ reforms: policyengine_us.reforms.congress.watca.working_americans_tax_cut_act.watca_reform_object input: gov.contrib.congress.watca.in_effect: true - adjusted_gross_income: 30_000 - filing_status: JOINT - head_is_dependent_elsewhere: true - spouse_is_dependent_elsewhere: true + people: + head: + is_tax_unit_head: true + claimed_as_dependent_on_another_return: true + spouse: + is_tax_unit_spouse: true + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 30_000 + filing_status: JOINT output: watca_alternative_tax_eligible: false diff --git a/policyengine_us/tests/policy/contrib/states/ct/hb5114/ct_hb5114.yaml b/policyengine_us/tests/policy/contrib/states/ct/hb5114/ct_hb5114.yaml index 9557ae1102e..bc07699ca08 100644 --- a/policyengine_us/tests/policy/contrib/states/ct/hb5114/ct_hb5114.yaml +++ b/policyengine_us/tests/policy/contrib/states/ct/hb5114/ct_hb5114.yaml @@ -427,11 +427,11 @@ age: 20 employment_income: 30_000 rent: 12_000 + claimed_as_dependent_on_another_return: true tax_units: tax_unit: members: [person1] filing_status: SINGLE - head_is_dependent_elsewhere: true households: household: members: [person1] diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py index 053ca2799c0..9cb742b764f 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py @@ -33,8 +33,12 @@ def formula(tax_unit, period, parameters): # IRC § 32(c)(1)(A)(ii)(III): without a qualifying child, the filer # must not be a dependent of another taxpayer; on a joint return, # "neither you nor your spouse can be claimed as a dependent by another - # person" (IRS Publication 596). - dependent_elsewhere = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + # person" (IRS Publication 596). A nonrequired claimant who files + # no return or only for a withholding/estimated-payment refund is + # excepted, without changing the standard-deduction limitation. + dependent_elsewhere = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) childless_eligible = ( tax_unit.any(meets_age_requirements & is_filer_or_spouse) & ~dependent_elsewhere diff --git a/policyengine_us/variables/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.py b/policyengine_us/variables/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.py index 6169e38bb5e..af27ae56bd4 100644 --- a/policyengine_us/variables/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.py +++ b/policyengine_us/variables/gov/states/az/tax/income/exemptions/az_aged_exemption_eligible_person.py @@ -8,6 +8,7 @@ class az_aged_exemption_eligible_person(Variable): unit = USD definition_period = YEAR defined_for = StateCode.AZ + reference = "https://azdor.gov/sites/default/files/document/FORMS_INDIVIDUAL_2025_140i.pdf#page=6" def formula(person, period, parameters): head = person("is_tax_unit_head", period) @@ -17,9 +18,6 @@ def formula(person, period, parameters): filing_status = tax_unit("az_filing_status", period) separate = filing_status == filing_status.possible_values.SEPARATE - dependent_head = tax_unit("head_is_dependent_elsewhere", period) - - dependent_spouse = tax_unit("spouse_is_dependent_elsewhere", period) - spouse_eligible_condition = ~dependent_spouse * ~separate - - return (head & ~dependent_head) | (spouse & spouse_eligible_condition) + # Form 140, Box 8, excludes each spouse only when actually claimed. + claimed_elsewhere = person("claimed_as_dependent_on_another_return", period) + return ~claimed_elsewhere & (head | (spouse & ~separate)) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py index 220561807b8..a1c5b5ff4aa 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit.py @@ -16,8 +16,9 @@ class hi_food_excise_credit(Variable): def formula(tax_unit, period, parameters): # HRS 235-55.85(a): only a taxpayer who cannot be claimed as a # dependent may claim the credit, for each qualified exemption; the - # exemption amount counts only filers who cannot be claimed when - # either can. Minor children receiving public support count for this + # exemption amount excludes own ordinary dependents when either + # filer can be claimed without the Dependent Taxpayer Test exception. + # Minor children receiving public support count for this # credit under 235-55.85(c), so they keep their amount unless every # filer can be claimed. every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py index c4cc41a8d42..15cb4a0c8de 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_credit_minor_child_count.py @@ -7,6 +7,10 @@ class hi_food_excise_credit_minor_child_count(Variable): label = "Minor child's number for the Hawaii Food/Excise Tax Credit" definition_period = YEAR defined_for = StateCode.HI + reference = ( + "https://files.hawaii.gov/tax/forms/2022/n311_i.pdf#page=1", + "https://files.hawaii.gov/tax/forms/2025/n311_i.pdf#page=1", + ) def formula(tax_unit, period, parameters): p = parameters(period).gov.states.hi.tax.income.credits.food_excise_tax @@ -19,10 +23,10 @@ def formula(tax_unit, period, parameters): # Form N-311 line 3 lists minor children who, among other tests, # "Cannot be claimed as a dependent by another taxpayer"; the filers # are listed on line 2. - claimed_elsewhere = person("claimed_as_dependent_on_another_return", period) + claimable_elsewhere = person("claimable_as_dependent_on_another_return", period) filer = person("is_tax_unit_head_or_spouse", period) eligible_minor_child = ( - is_child & minor & public_support_over_half & ~claimed_elsewhere & ~filer + is_child & minor & public_support_over_half & ~claimable_elsewhere & ~filer ) return tax_unit.sum(eligible_minor_child) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py index c597ba1352b..09127fcd2c1 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py @@ -9,6 +9,7 @@ class hi_food_excise_exemption_amount(Variable): defined_for = StateCode.HI reference = ( "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=50", + "https://files.hawaii.gov/tax/forms/2022/n311_i.pdf#page=1", "https://files.hawaii.gov/tax/forms/2025/n311_i.pdf#page=1", "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", ) @@ -18,7 +19,6 @@ def formula(tax_unit, period, parameters): income = tax_unit("adjusted_gross_income", period) - minor_child_include = p.minor_child.in_effect # Count exemptions as number of people # The legal code defines exemptions as Hawaii exemptions, # which normally include additional exemptions for aged and @@ -43,23 +43,34 @@ def formula(tax_unit, period, parameters): p.amount.surviving_spouse.calc(income), ], ) - exemptions = tax_unit("exemptions_count", period) + # Both 2022 and 2025 N-311 lines 2-3 exclude every person who can + # be claimed by another taxpayer, including children. In 2022, + # lines 8-10 price ordinary persons and publicly supported minors + # separately; 2025 lines 8-9 combine the two qualified counts. + externally_claimable = tax_unit.sum( + tax_unit.members("claimable_as_dependent_on_another_return", period) + ) + exemptions = max_( + tax_unit("exemptions_count", period) - externally_claimable, 0 + ) minor_children = tax_unit("hi_food_excise_credit_minor_child_count", period) if p.minor_child.in_effect: # Reduce number of exemptions by the number of minor children, # who get a fixed amount instead. - claimable_exemptions = exemptions - minor_children + claimable_exemptions = max_(exemptions - minor_children, 0) minor_child_exemptions = 0 else: claimable_exemptions = exemptions minor_child_exemptions = minor_children # Form N-311 counts only people who cannot be claimed as a dependent # by another taxpayer. A return on which a filer can be claimed - # generally claims no dependents (IRS Publication 501; its exception - # for a claimer who files only for a refund is not modeled), so it - # counts the filers who cannot be claimed and, through HRS - # 235-55.85(c), their minor children receiving public support. - dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + # generally claims no dependents (IRS Publication 501, Dependent + # Taxpayer Test). Its filing exception restores ordinary dependent + # entries, but never a claimable person's own exemption. Publicly + # supported minors count independently under HRS 235-55.85(c). + dependent_taxpayer = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period ) @@ -67,6 +78,8 @@ def formula(tax_unit, period, parameters): independent_filers > 0, minor_child_exemptions, 0 ) qualified_exemptions = where( - dependent_filer, mixed_return_exemptions, claimable_exemptions + dependent_taxpayer, mixed_return_exemptions, claimable_exemptions ) + every_filer_dependent = tax_unit("every_filer_is_dependent_elsewhere", period) + qualified_exemptions = where(every_filer_dependent, 0, qualified_exemptions) return qualified_exemptions * amount_per_exemption diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py index 734e6305b38..adc6f987c58 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py @@ -13,6 +13,7 @@ class hi_act_115_rebate(Variable): "https://data.capitol.hawaii.gov/sessions/session2022/bills/SB514_CD2_.HTM", "https://files.hawaii.gov/tax/news/announce/ann22-03.pdf#page=2", "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=45", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", ) def formula(tax_unit, period, parameters): @@ -44,12 +45,23 @@ def formula(tax_unit, period, parameters): # taxpayer claims the refund for the exemptions they are entitled to. # A filer who can be claimed has no exemption (HRS 235-54(a)), and a # return on which a filer can be claimed generally claims no dependents - # (IRS Publication 501; its exception for a claimer who files only for - # a refund is not modeled), so such a return counts only the filers - # who cannot be claimed. + # (IRS Publication 501, Dependent Taxpayer Test). Its filing exception + # restores those dependents, but does not restore the claimable + # filer's personal exemption or qualification as a claimant. dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period ) - qualified_exemptions = where(dependent_filer, independent_filers, exemptions) + dependent_taxpayer = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) + filers = add(tax_unit, period, ["is_tax_unit_head_or_spouse"]) + dependent_filers = filers - independent_filers + ordinary_exemptions = max_(exemptions - dependent_filers, 0) + qualified_exemptions = where( + dependent_taxpayer, independent_filers, ordinary_exemptions + ) + qualified_exemptions = where( + dependent_filer & (independent_filers == 0), 0, qualified_exemptions + ) return amount_per_exemption * qualified_exemptions diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py index f60088206d8..a744ce369bf 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py @@ -11,6 +11,7 @@ class hi_tax_credit_for_low_income_household_renters(Variable): reference = ( "https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=48", "https://files.hawaii.gov/tax/forms/2025/schx_i.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", ) def formula(tax_unit, period, parameters): @@ -19,9 +20,10 @@ def formula(tax_unit, period, parameters): # Schedule X lines 8-10 count only people who cannot be claimed as a # dependent by another taxpayer, with an extra exemption for each # such filer aged 65 or over. A return on which a filer can be - # claimed generally claims no dependents (IRS Publication 501; its - # exception for a claimer who files only for a refund is not modeled), - # so it counts only the filers who cannot be claimed. + # claimed generally claims no dependents (IRS Publication 501, + # Dependent Taxpayer Test). Its filing exception restores the + # return's own dependents, while claimable filers and their age + # additions remain excluded. aged_head = (tax_unit("age_head", period) >= p.aged_age_threshold) & ~tax_unit( "head_is_dependent_elsewhere", period ) @@ -30,15 +32,18 @@ def formula(tax_unit, period, parameters): ) & ~tax_unit("spouse_is_dependent_elsewhere", period) aged_exemptions = aged_head.astype(int) + aged_spouse.astype(int) - dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + dependent_taxpayer = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period ) - exemptions = where( - dependent_filer, - independent_filers, - tax_unit("exemptions_count", period), + filers = add(tax_unit, period, ["is_tax_unit_head_or_spouse"]) + dependent_filers = filers - independent_filers + ordinary_exemptions = max_( + tax_unit("exemptions_count", period) - dependent_filers, 0 ) + exemptions = where(dependent_taxpayer, independent_filers, ordinary_exemptions) total_exemptions = exemptions + aged_exemptions return p.amount * total_exemptions diff --git a/policyengine_us/variables/gov/states/me/tax/income/credits/me_affordability_payment.py b/policyengine_us/variables/gov/states/me/tax/income/credits/me_affordability_payment.py index b488cf00424..7f1a547ff0e 100644 --- a/policyengine_us/variables/gov/states/me/tax/income/credits/me_affordability_payment.py +++ b/policyengine_us/variables/gov/states/me/tax/income/credits/me_affordability_payment.py @@ -14,7 +14,7 @@ class me_affordability_payment(Variable): "payment from a Special Revenue Fund rather than a refundable tax " "credit. PolicyEngine folds the payment into Maine's refundable " "credit aggregation to surface it in tax-unit-level outputs. " - "Sec. T-1, sub-§1(C)(3) excludes individuals claimed as a " + "Sec. T-1, sub-§1(C)(3) excludes individuals who may be claimed as a " "dependent on another taxpayer's return, evaluated per recipient." ) @@ -24,6 +24,6 @@ def formula(tax_unit, period, parameters): is_recipient = person("is_tax_unit_head", period) | person( "is_tax_unit_spouse", period ) - dependent_elsewhere = person("claimed_as_dependent_on_another_return", period) + dependent_elsewhere = person("claimable_as_dependent_on_another_return", period) eligible_recipients = tax_unit.sum(is_recipient & ~dependent_elsewhere) return eligible_recipients * p.amount diff --git a/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py b/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py index 1940d6d1af8..7c9874052ea 100644 --- a/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py +++ b/policyengine_us/variables/gov/states/mi/tax/income/exemptions/mi_exemptions.py @@ -40,8 +40,9 @@ def formula(tax_unit, period, parameters): # individual, we apply it to each spouse, so the other keeps their own # personal exemption (and any stillbirth exemption). A joint return on # which either spouse can be claimed generally claims no dependents - # (IRS Publication 501, "Dependent Taxpayer Test"; its exception for a - # claimer who files only for a refund is not modeled). + # (IRS Publication 501, "Dependent Taxpayer Test"). The filing + # exception restores the return's own dependents, while each + # claimable filer keeps the $1,500 allowance. filers = add(tax_unit, period, ["is_tax_unit_head_or_spouse"]) independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period @@ -53,11 +54,19 @@ def formula(tax_unit, period, parameters): ) * p.personal dependent_filer_exemptions = dependent_filers * p.dependent_on_other_return + dependent_taxpayer = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) + own_dependents = max_(tax_unit("tax_unit_size", period) - filers, 0) + dependent_exemptions = where(dependent_taxpayer, 0, own_dependents) * p.personal + # Total exemptions return ( where( dependent_filers > 0, - independent_filer_exemptions + dependent_filer_exemptions, + independent_filer_exemptions + + dependent_filer_exemptions + + dependent_exemptions, personal_exemption, ) + disabled_exemption diff --git a/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py b/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py index ef104baa938..416d57eeb7a 100644 --- a/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py +++ b/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py @@ -33,7 +33,7 @@ def formula(tax_unit, period, parameters): people = tax_unit.members is_dependent = people("is_tax_unit_dependent", period) | people( - "claimed_as_dependent_on_another_return", period + "claimable_as_dependent_on_another_return", period ) social_security = people("social_security", period) taxable_social_security = people("taxable_social_security", period) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py index 1ee85c8bfae..5f7a3dbbec3 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py @@ -26,7 +26,7 @@ def formula(tax_unit, period, parameters): # dependent claim it when the other spouse is. So when a filer is a # dependent elsewhere, a filer who is not must be 65 or older. filer = person("is_tax_unit_head_or_spouse", period) - claimed = person("claimed_as_dependent_on_another_return", period) + claimed = person("claimable_as_dependent_on_another_return", period) independent_aged_filer = tax_unit.any(filer & ~claimed & aged) dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) age_eligible = where( diff --git a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py index e222b7bf362..9a45b4bda79 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py @@ -16,7 +16,7 @@ def formula(person, period, parameters): # if they are not claimed as a dependent elsewhere head_or_spouse = person("is_tax_unit_head_or_spouse", period) dependent_on_another_return = person( - "claimed_as_dependent_on_another_return", period + "claimable_as_dependent_on_another_return", period ) # The personal exemption is also provided to dependents dependent = person("is_tax_unit_dependent", period) diff --git a/policyengine_us/variables/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.py b/policyengine_us/variables/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.py index 28c3dbbdf03..146db78e423 100644 --- a/policyengine_us/variables/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.py +++ b/policyengine_us/variables/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief_pre_one_claimant_eligible.py @@ -36,9 +36,12 @@ def formula(tax_unit, period, parameters): claimant_is_tax_unit_dependent = tax_unit.any( claimants & tax_unit.members("is_tax_unit_dependent", period) ) - claimant_is_dependent_elsewhere = tax_unit( - "head_is_dependent_elsewhere", period - ) | tax_unit("spouse_is_dependent_elsewhere", period) + # Utah Code 59-2a-205(4) and 59-2a-305(2)(a) turn on another + # taxpayer actually claiming the exemption or dependent credit. + claimant_is_dependent_elsewhere = tax_unit.any( + claimants + & tax_unit.members("claimed_as_dependent_on_another_return", period) + ) claimant_is_dependent = ( claimant_is_tax_unit_dependent | claimant_is_dependent_elsewhere ) diff --git a/policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py b/policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py new file mode 100644 index 00000000000..eccd696bbec --- /dev/null +++ b/policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class claimable_as_dependent_on_another_return(Variable): + value_type = bool + entity = Person + definition_period = YEAR + label = "Can be claimed as a dependent elsewhere" + documentation = ( + "Whether another taxpayer can claim the person as a dependent: a " + "deduction under IRC section 151 for the person is allowable to " + "another taxpayer, whether or not claimed. Defaults to the actual " + "or expected claim input for compatibility with existing callers. " + "The dependent_claimant_filing_exception does not change this fact " + "or the dependent standard-deduction limit." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/151#c", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=24", + ) + + def formula(person, period, parameters): + return person("claimed_as_dependent_on_another_return", period) diff --git a/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py b/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py index f941fcb3254..cfaa9d32825 100644 --- a/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py +++ b/policyengine_us/variables/household/demographic/person/claimed_as_dependent_on_another_return.py @@ -7,11 +7,8 @@ class claimed_as_dependent_on_another_return(Variable): definition_period = YEAR label = "Is claimed as a dependent elsewhere" documentation = ( - "Whether another taxpayer can claim the person as a dependent, that " - "is, a deduction under IRC section 151 for the person is allowable to " - "another taxpayer, whether or not it is claimed. A person whose " - "would-be claimer is not required to file and files only to claim a " - "refund is not such a dependent (IRS Publications 501 and 596). Rules " - "that turn on actually being claimed also read this input; the model " - "has no separate input for that." + "Whether the person is actually claimed, or expected to be claimed, " + "as a dependent in another tax unit. Claimability, whether or not " + "another taxpayer claims the person, is represented separately by " + "claimable_as_dependent_on_another_return." ) diff --git a/policyengine_us/variables/household/demographic/person/dependent_claimant_filing_exception.py b/policyengine_us/variables/household/demographic/person/dependent_claimant_filing_exception.py new file mode 100644 index 00000000000..d4acf034fba --- /dev/null +++ b/policyengine_us/variables/household/demographic/person/dependent_claimant_filing_exception.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class dependent_claimant_filing_exception(Variable): + value_type = bool + entity = Person + definition_period = YEAR + label = "Dependent's would-be claimant meets the filing exception" + documentation = ( + "Whether the person who could claim this person (all such claimants " + "if more than one) is not required to file an income tax return and " + "either files no return or files solely to obtain a refund of income " + "tax withheld or estimated tax paid. A return filed to claim refundable " + "tax credits, including EITC, does not qualify. This fact permits EITC and claiming " + "one's own dependents under the Dependent Taxpayer Test; it does not " + "remove general claimability or its standard-deduction limitation." + ) + reference = ( + "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=18", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", + ) diff --git a/policyengine_us/variables/household/demographic/person/is_dependent_elsewhere_without_filing_exception.py b/policyengine_us/variables/household/demographic/person/is_dependent_elsewhere_without_filing_exception.py new file mode 100644 index 00000000000..79384c81b5e --- /dev/null +++ b/policyengine_us/variables/household/demographic/person/is_dependent_elsewhere_without_filing_exception.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class is_dependent_elsewhere_without_filing_exception(Variable): + value_type = bool + entity = Person + definition_period = YEAR + label = "Claimable elsewhere without the claimant filing exception" + documentation = ( + "Whether the person is claimable as a dependent on another return " + "and the would-be claimant does not meet the nonrequired-filing " + "exception. Used for childless EITC and the Dependent Taxpayer Test." + ) + reference = ( + "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=18", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", + ) + + def formula(person, period, parameters): + return person("claimable_as_dependent_on_another_return", period) & ~person( + "dependent_claimant_filing_exception", period + ) diff --git a/policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py index 161339f0358..e2135a89019 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py +++ b/policyengine_us/variables/household/demographic/tax_unit/every_filer_is_dependent_elsewhere.py @@ -5,9 +5,9 @@ class every_filer_is_dependent_elsewhere(Variable): value_type = bool entity = TaxUnit definition_period = YEAR - label = "Every head and spouse is a dependent elsewhere" + label = "Every head and spouse is claimable elsewhere" documentation = ( - "Whether the tax unit has a head or spouse claimed as a dependent in " + "Whether the tax unit has a head or spouse claimable as a dependent in " "another tax unit and none who is not. False for a tax unit with " "neither a head nor a spouse." ) diff --git a/policyengine_us/variables/household/demographic/tax_unit/head_is_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/head_is_dependent_elsewhere.py index 04064b39948..b8d43c53e5e 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/head_is_dependent_elsewhere.py +++ b/policyengine_us/variables/household/demographic/tax_unit/head_is_dependent_elsewhere.py @@ -6,12 +6,12 @@ class head_is_dependent_elsewhere(Variable): entity = TaxUnit definition_period = YEAR label = "Is tax-unit head a dependent elsewhere" - documentation = "Whether the filer for this tax unit is claimed as a dependent in another tax unit." + documentation = "Whether the filer for this tax unit is claimable as a dependent in another tax unit." def formula(tax_unit, period, parameters): person = tax_unit.members - claimed_on_another_return = person( - "claimed_as_dependent_on_another_return", period + claimable_on_another_return = person( + "claimable_as_dependent_on_another_return", period ) is_head = person("is_tax_unit_head", period) - return tax_unit.any(claimed_on_another_return & is_head) + return tax_unit.any(claimable_on_another_return & is_head) diff --git a/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py index 493bf92c3c4..34f5593350b 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py +++ b/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere.py @@ -5,17 +5,17 @@ class head_or_spouse_is_dependent_elsewhere(Variable): value_type = bool entity = TaxUnit definition_period = YEAR - label = "Head or spouse is a dependent elsewhere" + label = "Head or spouse is claimable elsewhere" documentation = ( - "Whether the head or the spouse of this tax unit is claimed as a " + "Whether the head or the spouse of this tax unit is claimable as a " "dependent in another tax unit. Unlike head_is_dependent_elsewhere, " "this does not depend on which spouse is labelled head." ) def formula(tax_unit, period, parameters): person = tax_unit.members - claimed_on_another_return = person( - "claimed_as_dependent_on_another_return", period + claimable_on_another_return = person( + "claimable_as_dependent_on_another_return", period ) filer = person("is_tax_unit_head_or_spouse", period) - return tax_unit.any(claimed_on_another_return & filer) + return tax_unit.any(claimable_on_another_return & filer) diff --git a/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.py b/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.py new file mode 100644 index 00000000000..26a1d3d3eb4 --- /dev/null +++ b/policyengine_us/variables/household/demographic/tax_unit/head_or_spouse_is_dependent_elsewhere_without_filing_exception.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class head_or_spouse_is_dependent_elsewhere_without_filing_exception(Variable): + value_type = bool + entity = TaxUnit + definition_period = YEAR + label = "Either filer claimable elsewhere without the claimant filing exception" + documentation = ( + "Whether a tax-unit head or spouse is claimable on another return " + "without the claimant filing exception. Symmetric Dependent Taxpayer " + "Test gate; other claimability rules use the ordinary helper." + ) + reference = ( + "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=18", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + dependent = person("is_dependent_elsewhere_without_filing_exception", period) + filer = person("is_tax_unit_head_or_spouse", period) + return tax_unit.any(dependent & filer) diff --git a/policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py index 348b7a993a2..3d040cf683d 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py +++ b/policyengine_us/variables/household/demographic/tax_unit/head_spouse_count_not_dependent_elsewhere.py @@ -7,14 +7,14 @@ class head_spouse_count_not_dependent_elsewhere(Variable): definition_period = YEAR label = "Head and spouse count not dependent elsewhere" documentation = ( - "Number of tax unit heads and spouses who are not claimed as a " + "Number of tax unit heads and spouses who are not claimable as a " "dependent in another tax unit." ) def formula(tax_unit, period, parameters): person = tax_unit.members - claimed_on_another_return = person( - "claimed_as_dependent_on_another_return", period + claimable_on_another_return = person( + "claimable_as_dependent_on_another_return", period ) filer = person("is_tax_unit_head_or_spouse", period) - return tax_unit.sum(filer & ~claimed_on_another_return) + return tax_unit.sum(filer & ~claimable_on_another_return) diff --git a/policyengine_us/variables/household/demographic/tax_unit/spouse_is_dependent_elsewhere.py b/policyengine_us/variables/household/demographic/tax_unit/spouse_is_dependent_elsewhere.py index b8fb0bc9c5f..a8e0183e464 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/spouse_is_dependent_elsewhere.py +++ b/policyengine_us/variables/household/demographic/tax_unit/spouse_is_dependent_elsewhere.py @@ -6,12 +6,12 @@ class spouse_is_dependent_elsewhere(Variable): entity = TaxUnit definition_period = YEAR label = "Tax-unit spouse is dependent elsewhere" - documentation = "Whether the spouse of the filer for this tax unit is claimed as a dependent in another tax unit." + documentation = "Whether the spouse of the filer for this tax unit is claimable as a dependent in another tax unit." def formula(tax_unit, period, parameters): person = tax_unit.members - claimed_on_another_return = person( - "claimed_as_dependent_on_another_return", period + claimable_on_another_return = person( + "claimable_as_dependent_on_another_return", period ) is_spouse = person("is_tax_unit_spouse", period) - return tax_unit.any(claimed_on_another_return & is_spouse) + return tax_unit.any(claimable_on_another_return & is_spouse) From c7f969f872be92689e1606f0b564cb3c8a121d16 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:16:55 -0400 Subject: [PATCH 14/32] Clarify claimability outside the current tax unit --- .../person/claimable_as_dependent_on_another_return.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py b/policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py index eccd696bbec..5643c2eb30d 100644 --- a/policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py +++ b/policyengine_us/variables/household/demographic/person/claimable_as_dependent_on_another_return.py @@ -7,7 +7,8 @@ class claimable_as_dependent_on_another_return(Variable): definition_period = YEAR label = "Can be claimed as a dependent elsewhere" documentation = ( - "Whether another taxpayer can claim the person as a dependent: a " + "Whether another taxpayer, in another tax unit, can claim the person " + "as a dependent: a " "deduction under IRC section 151 for the person is allowable to " "another taxpayer, whether or not claimed. Defaults to the actual " "or expected claim input for compatibility with existing callers. " From 3f8870261ca2ff4ec7c4b28e106d6c4cadcec935 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:39:43 -0400 Subject: [PATCH 15/32] Read the split dependency inputs from #10024 #10024 now separates three facts: is claimed, can be claimed, and the would-be claimant's filing exception. - IRC 152(b)(1) gates (child tax credit, credit for other dependents, under-13 care child and its expenses, dependents in exemptions_count, ARPA dependents, Ohio dependents, the Hawaii and Wisconsin reforms) read head_or_spouse_is_dependent_elsewhere_without_filing_exception, so a filer within the Publication 501 exception keeps their dependents. - Own exemptions, recovery rebates and the tuition deduction read claimability. - Education credits and student loan interest keep reading the claimed input. - Tests: the filing exception and a filer who can be claimed but is not. Co-Authored-By: Claude Opus 5.5 --- .../hi_dependent_exemption_reform.py | 4 +- .../wi_dependent_exemption_reform.py | 4 +- .../credits/cdcc/cdcc_relevant_expenses.yaml | 29 +++++ .../ctc/maximum/ctc_qualifying_child.yaml | 84 ++++++++++++++ ...gible_for_american_opportunity_credit.yaml | 26 ++++- .../recovery_rebate_credit.yaml | 19 +++ .../student_loan_interest_ald_eligible.yaml | 19 ++- .../tuition_and_fees_deduction_eligible.yaml | 18 +++ .../exemptions/exemptions_count.yaml | 109 ++++++++++++++++++ .../gov/irs/credits/cdcc/cdcc_eligible.py | 2 +- .../credits/cdcc/cdcc_relevant_expenses.py | 4 +- .../ctc/maximum/ctc_qualifying_child.py | 2 +- .../ctc_adult_individual_maximum.py | 2 +- .../rrc_adult_count_with_valid_ssn.py | 4 +- .../recovery_rebate_credit/rrc_arpa.py | 2 +- .../rrc_arpa_dependents_with_valid_ssn.py | 2 +- .../credits/recovery_rebate_credit/rrc_caa.py | 2 +- .../recovery_rebate_credit/rrc_cares.py | 2 +- .../exemptions/exemptions_count.py | 22 ++-- .../oh_personal_exemptions_eligible_person.py | 2 +- 20 files changed, 332 insertions(+), 26 deletions(-) diff --git a/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py b/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py index 3349f9a9f31..d1b0283f0b2 100644 --- a/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py +++ b/policyengine_us/reforms/states/hi/dependent_exemption/hi_dependent_exemption_reform.py @@ -18,7 +18,7 @@ def formula(tax_unit, period, parameters): # A return on which the filer (or, if joint, either spouse) can be # claimed as a dependent has no dependents (IRC 152(b)(1)). is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) # Apply age limit if in effect @@ -56,7 +56,7 @@ def formula(tax_unit, period, parameters): # A return on which the filer (or, if joint, either spouse) can be # claimed as a dependent has no dependents (IRC 152(b)(1)). is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) total_dependents = tax_unit.sum(is_dependent) eligible_dependent_exemptions = tax_unit( diff --git a/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py b/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py index 85e40eefe7f..0471bac9c9a 100644 --- a/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py +++ b/policyengine_us/reforms/states/wi/dependent_exemption/wi_dependent_exemption_reform.py @@ -18,7 +18,7 @@ def formula(tax_unit, period, parameters): # A return on which the filer (or, if joint, either spouse) can be # claimed as a dependent has no dependents (IRC 152(b)(1)). is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) # Apply age limit if in effect @@ -56,7 +56,7 @@ def formula(tax_unit, period, parameters): # A return on which the filer (or, if joint, either spouse) can be # claimed as a dependent has no dependents (IRC 152(b)(1)). is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) total_dependents = tax_unit.sum(is_dependent) eligible_dependent_exemptions = tax_unit( diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml index 15dfeb1fe4a..11084acd2b8 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml @@ -209,3 +209,32 @@ output: count_cdcc_eligible: 2 cdcc_relevant_expenses: 3000 + +# Publication 501, Dependent Taxpayer Test: a filer whose would-be claimant is not +# required to file and files no return or only for a refund can claim dependents. +- name: 2025 childcare for the child of a claimable filer within the filing exception counts + period: 2025 + input: + people: + parent: + age: 30 + employment_income: 5000 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child] + tax_unit_childcare_expenses: 3000 + households: + household: + members: [parent, child] + state_code: TX + output: + count_cdcc_eligible: 1 + cdcc_relevant_expenses: 3000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml index 0a40c086281..fc96d52a969 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/ctc_qualifying_child.yaml @@ -159,3 +159,87 @@ output: ctc_qualifying_child: [false, false, true] ctc_maximum: 2_200 + +# Publication 501, Dependent Taxpayer Test: a filer whose would-be claimant is not +# required to file and files no return or only for a refund can claim dependents. +- name: 2025 claimable single filer within the filing exception keeps the CTC-qualifying child + period: 2025 + input: + people: + parent: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + ctc_qualifying_child: [false, true] + +- name: 2025 joint return whose claimable spouse is within the filing exception keeps the CTC-qualifying child (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + ctc_qualifying_child: [false, false, true] + +- name: 2025 joint return whose claimable spouse is within the filing exception keeps the CTC-qualifying child (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + ctc_qualifying_child: [false, false, true] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml index 908d3d3c657..b932cf52198 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml @@ -178,7 +178,6 @@ output: is_eligible_for_american_opportunity_credit: false - - name: 2024 student claimed as a dependent elsewhere cannot claim the credit period: 2024 input: @@ -307,3 +306,28 @@ state_code: TX output: is_eligible_for_american_opportunity_credit: [true, true] + +# IRC 25A(g)(3) and 26 CFR 1.25A-1(f): the bar applies to a student who is +# claimed; a student who can be claimed but is not keeps the credit. +- name: 2024 student who can be claimed but is not claimed keeps the credit + period: 2024 + input: + people: + person_a: + age: 20 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + households: + household: + members: [person_a] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml index 045236d3234..2aa4b19bdf1 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml @@ -311,3 +311,22 @@ state_code: TX output: rrc_arpa: 2800 + +# IRC 6428B(c)(2): an individual "who is a dependent of another taxpayer" is not +# eligible, whether or not that taxpayer claims them. +- name: 2021 single filer who can be claimed but is not claimed gets no ARPA rebate + period: 2021 + input: + people: + person_a: + age: 20 + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + households: + household: + members: [person_a] + state_code: TX + output: + rrc_arpa: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml index 97566eae089..e161077097f 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/student_loan_interest/student_loan_interest_ald_eligible.yaml @@ -38,7 +38,6 @@ output: student_loan_interest_ald_eligible: false - - name: 2024 single filer claimed as a dependent elsewhere cannot deduct student loan interest period: 2024 input: @@ -106,3 +105,21 @@ state_code: TX output: student_loan_interest_ald_eligible: [false, false] + +# IRC 221(c): the bar applies to an individual who is claimed. +- name: 2024 single filer who can be claimed but is not claimed can deduct student loan interest + period: 2024 + input: + people: + person_a: + age: 22 + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + households: + household: + members: [person_a] + state_code: TX + output: + student_loan_interest_ald_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml index dd6bcd8d147..c55f696055c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tuition_and_fees_deduction_eligible.yaml @@ -116,3 +116,21 @@ state_code: TX output: tuition_and_fees_deduction_eligible: true + +- name: 2020 single filer who can be claimed but is not claimed is not eligible + period: 2020 + input: + people: + person_a: + age: 20 + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + households: + household: + members: [person_a] + state_code: TX + output: + # IRC 222(c)(3): a section 151 deduction "may be allowed to another taxpayer". + tuition_and_fees_deduction_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml index 16c899b35cf..40ce1bf5142 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/exemptions/exemptions_count.yaml @@ -131,3 +131,112 @@ state_code: TX output: exemptions_count: 3 + +# Publication 501, Dependent Taxpayer Test: a filer whose would-be claimant is not +# required to file and files no return or only for a refund can claim dependents. +# The filer's own exemption is still zero (IRC 151(d)(2)). +- name: 2025 claimable single filer within the filing exception counts the child but not themselves + period: 2025 + input: + people: + parent: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + exemptions_count: 1 + +- name: 2025 joint return whose claimable spouse is within the filing exception counts the other spouse and the child (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + exemptions_count: 2 + +- name: 2025 joint return whose claimable spouse is within the filing exception counts the other spouse and the child (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + exemptions_count: 2 + +- name: 2025 filer who can be claimed but is not claimed has no own exemption and no dependents + period: 2025 + input: + people: + parent: + age: 30 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # Exemptions turn on whether the filer can be claimed, not on an actual claim. + exemptions_count: 0 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py index 7e45d74c8f1..c7dbc104092 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py @@ -31,7 +31,7 @@ def formula(person, period, parameters): # input for an IRC 152(e) release, so the 21(e)(5) child of a # custodial parent who can be claimed is not kept. filer_is_dependent = person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) qualifies_by_age = ~head_or_spouse & (age < p.child_age) & ~filer_is_dependent # Subsections (b)(1)(B) (dependent) and (b)(1)(C) (spouse). Subsection diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py index 501d124e5b6..fba13665ad0 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py @@ -28,7 +28,9 @@ def formula(tax_unit, period, parameters): qualifying_child = person("is_cdcc_eligible", period) & ( person("age", period) < p.child_age ) - dependent_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + dependent_filer = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) childcare = where( dependent_filer & ~tax_unit.any(qualifying_child), 0, childcare ) diff --git a/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py b/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py index 3bc002458d2..f8906588f5d 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/maximum/ctc_qualifying_child.py @@ -30,6 +30,6 @@ def formula(person, period, parameters): # having no dependents, and on a joint return neither spouse may be # claimable (Publication 501, Dependent Taxpayer Test). filer_is_dependent = person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) return age_eligible & meets_identification_requirements & ~filer_is_dependent diff --git a/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py b/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py index 23c93da7ba3..13c2ffd4c52 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.py @@ -29,7 +29,7 @@ def formula(person, period, parameters): # under IRC 152(b)(1) a return on which the filer (or, if joint, # either spouse) can be claimed as a dependent has no dependents. filer_is_dependent = person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) return ( is_adult diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py index 7ef7231bb3b..63612661479 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py @@ -24,5 +24,5 @@ def formula(tax_unit, period, parameters): # whom a deduction under section 151 is allowable to another # taxpayer" (IRC 6428(d)(2), 6428A(d)(2)) or "who is a dependent of # another taxpayer" (IRC 6428B(c)(2)): a claimability test. - claimed = person("claimed_as_dependent_on_another_return", period) - return tax_unit.sum(head_or_spouse & has_valid_ssn & ~claimed) + claimable = person("claimable_as_dependent_on_another_return", period) + return tax_unit.sum(head_or_spouse & has_valid_ssn & ~claimable) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py index 2ea2796ed3c..0ffd5e954b4 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py @@ -31,7 +31,7 @@ def formula(tax_unit, period, parameters): person = tax_unit.members dependent_filers = tax_unit.sum( person("is_tax_unit_head_or_spouse", period) - & person("claimed_as_dependent_on_another_return", period) + & person("claimable_as_dependent_on_another_return", period) ) count_adults = where( armed_forces_exception, diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py index e91e2ee9a48..45245ce16a2 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa_dependents_with_valid_ssn.py @@ -17,7 +17,7 @@ def formula(tax_unit, period, parameters): # under 152(b)(1) a return on which the filer (or, if joint, either # spouse) can be claimed as a dependent has none. dependent_filer = person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) is_dependent = person("is_tax_unit_dependent", period) & ~dependent_filer has_valid_ssn = person("meets_eitc_identification_requirements", period) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py index cc704eb482b..b0ee925ced1 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py @@ -28,7 +28,7 @@ def formula(tax_unit, period, parameters): person = tax_unit.members dependent_filers = tax_unit.sum( person("is_tax_unit_head_or_spouse", period) - & person("claimed_as_dependent_on_another_return", period) + & person("claimable_as_dependent_on_another_return", period) ) count_adults = where( armed_forces_exception, diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py index be4a6d8c1c0..c61fca0c9a7 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py @@ -28,7 +28,7 @@ def formula(tax_unit, period, parameters): person = tax_unit.members dependent_filers = tax_unit.sum( person("is_tax_unit_head_or_spouse", period) - & person("claimed_as_dependent_on_another_return", period) + & person("claimable_as_dependent_on_another_return", period) ) count_adults = where( armed_forces_exception, diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py b/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py index 2fec52064f4..57eaacb22f8 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/exemptions/exemptions_count.py @@ -10,7 +10,8 @@ class exemptions_count(Variable): documentation = ( "Number of personal exemptions on the federal return: one for each " "filer who cannot be claimed as a dependent by another taxpayer, plus " - "one for each dependent unless a filer can be claimed." + "one for each dependent unless a filer can be claimed and the " + "would-be claimant does not meet the filing exception." ) reference = ( "https://www.law.cornell.edu/uscode/text/26/151#d_2", @@ -25,13 +26,16 @@ class exemptions_count(Variable): ) def formula(tax_unit, period, parameters): - # IRC 151(d)(2) zeroes the exemption of a filer whom another taxpayer - # can claim, and IRC 152(b)(1) treats a return on which the filer (or, - # if joint, either spouse) can be claimed as having no dependents. So - # such a return counts only its filers who cannot be claimed. tax_unit_size = tax_unit("tax_unit_size", period) - filer_is_dependent = tax_unit("head_or_spouse_is_dependent_elsewhere", period) - independent_filers = tax_unit( - "head_spouse_count_not_dependent_elsewhere", period + # IRC 151(d)(2): no exemption for a filer whom another taxpayer can + # claim as a dependent. + claimable_filer = tax_unit("head_or_spouse_is_dependent_elsewhere", period) + own = tax_unit("head_spouse_count_not_dependent_elsewhere", period) + # IRC 152(b)(1): such a filer has no dependents, unless the would-be + # claimant is not required to file and files no return or only for a + # refund (Publication 501, Dependent Taxpayer Test). + no_dependents = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) - return where(filer_is_dependent, independent_filers, tax_unit_size) + dependents = where(no_dependents, 0, tax_unit("tax_unit_dependents", period)) + return where(claimable_filer, own + dependents, tax_unit_size) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py index d19c249a8e9..813a61fd698 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions_eligible_person.py @@ -22,7 +22,7 @@ def formula(person, period, parameters): # on which the filer (or, if joint, either spouse) can be claimed as a # dependent has none (IRC 152(b)(1)). dependent_filer = person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) dependent = person("is_tax_unit_dependent", period) & ~dependent_filer return (~dependent_on_another_return & head_or_spouse) | dependent From b2771d2f15466f0850788b5577fe3f503681607f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 14:58:06 -0400 Subject: [PATCH 16/32] Follow the corrected exemption count in three Hawaii credits #10024's food/excise credit, Act 115 rebate and renters credit took claimable filers off exemptions_count themselves. exemptions_count now leaves them out (and, outside the filing exception, the return's dependents), so those credits read it as it is; the filing-exception cases were one exemption short. The renters credit also leaves out a dependent who can be claimed by another taxpayer (Schedule X counts only people who cannot be), as the food/excise credit already did. Co-Authored-By: Claude Opus 5.5 --- ...w_income_household_renters_tax_credit.yaml | 126 ++++++++++++++++++ .../hi_food_excise_exemption_amount.py | 10 +- .../tax/income/credits/hi_act_115_rebate.py | 14 +- ...low_income_household_renters_tax_credit.py | 20 ++- 4 files changed, 150 insertions(+), 20 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml index 25fa5e14ce5..cc6de0fb9ea 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.yaml @@ -166,3 +166,129 @@ state_code: HI output: hi_tax_credit_for_low_income_household_renters: 150 + +# Schedule X counts only people who cannot be claimed as a dependent by another +# taxpayer, so a child in the household who can be claimed elsewhere adds no +# exemption. +- name: Joint return with a child who can be claimed elsewhere counts the two filers + period: 2021 + input: + people: + person_a: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_tax_credit_for_low_income_household_renters: 100 + +- name: Joint return with a child who cannot be claimed elsewhere counts the child + period: 2021 + input: + people: + person_a: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_tax_credit_for_low_income_household_renters: 150 + +- name: Joint return within the filing exception with a child who can be claimed elsewhere counts only the other spouse (the claimable spouse is the head) + period: 2021 + input: + people: + person_a: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_a, person_b, child_1] + state_code: HI + output: + hi_tax_credit_for_low_income_household_renters: 50 + +- name: Joint return within the filing exception with a child who can be claimed elsewhere counts only the other spouse (the claimable spouse is the spouse) + period: 2021 + input: + people: + person_b: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child_1: + age: 5 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child_1] + filing_status: JOINT + adjusted_gross_income: 4000 + hi_tax_credit_for_low_income_household_renters_eligible: true + households: + household: + members: [person_b, person_a, child_1] + state_code: HI + output: + hi_tax_credit_for_low_income_household_renters: 50 diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py index 09127fcd2c1..39bbb252a55 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/food_excise/hi_food_excise_exemption_amount.py @@ -47,11 +47,15 @@ def formula(tax_unit, period, parameters): # be claimed by another taxpayer, including children. In 2022, # lines 8-10 price ordinary persons and publicly supported minors # separately; 2025 lines 8-9 combine the two qualified counts. - externally_claimable = tax_unit.sum( - tax_unit.members("claimable_as_dependent_on_another_return", period) + # exemptions_count already leaves out a filer who can be claimed, so + # only the dependents who can be claimed elsewhere come off here. + person = tax_unit.members + externally_claimable_dependents = tax_unit.sum( + person("is_tax_unit_dependent", period) + & person("claimable_as_dependent_on_another_return", period) ) exemptions = max_( - tax_unit("exemptions_count", period) - externally_claimable, 0 + tax_unit("exemptions_count", period) - externally_claimable_dependents, 0 ) minor_children = tax_unit("hi_food_excise_credit_minor_child_count", period) if p.minor_child.in_effect: diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py index adc6f987c58..f0c3d84ac13 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_act_115_rebate.py @@ -52,16 +52,10 @@ def formula(tax_unit, period, parameters): independent_filers = tax_unit( "head_spouse_count_not_dependent_elsewhere", period ) - dependent_taxpayer = tax_unit( - "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period - ) - filers = add(tax_unit, period, ["is_tax_unit_head_or_spouse"]) - dependent_filers = filers - independent_filers - ordinary_exemptions = max_(exemptions - dependent_filers, 0) - qualified_exemptions = where( - dependent_taxpayer, independent_filers, ordinary_exemptions - ) + # exemptions_count applies these rules: it leaves out a claimable + # filer's own exemption and, unless the filing exception applies, the + # return's dependents. qualified_exemptions = where( - dependent_filer & (independent_filers == 0), 0, qualified_exemptions + dependent_filer & (independent_filers == 0), 0, exemptions ) return amount_per_exemption * qualified_exemptions diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py index a744ce369bf..d55ac2607ca 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_low_income_household_renters/hi_low_income_household_renters_tax_credit.py @@ -32,18 +32,24 @@ def formula(tax_unit, period, parameters): ) & ~tax_unit("spouse_is_dependent_elsewhere", period) aged_exemptions = aged_head.astype(int) + aged_spouse.astype(int) + # exemptions_count applies these rules to the filers and to the + # return's dependents. A dependent who can be claimed by another + # taxpayer is also left out; a return without dependents has none to + # take off. dependent_taxpayer = tax_unit( "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) - independent_filers = tax_unit( - "head_spouse_count_not_dependent_elsewhere", period + person = tax_unit.members + externally_claimable_dependents = tax_unit.sum( + person("is_tax_unit_dependent", period) + & person("claimable_as_dependent_on_another_return", period) ) - filers = add(tax_unit, period, ["is_tax_unit_head_or_spouse"]) - dependent_filers = filers - independent_filers - ordinary_exemptions = max_( - tax_unit("exemptions_count", period) - dependent_filers, 0 + exemptions_count = tax_unit("exemptions_count", period) + exemptions = where( + dependent_taxpayer, + exemptions_count, + max_(exemptions_count - externally_claimable_dependents, 0), ) - exemptions = where(dependent_taxpayer, independent_filers, ordinary_exemptions) total_exemptions = exemptions + aged_exemptions return p.amount * total_exemptions From 163fcd4cfa98345d5ec0c358d2d35c8516789334 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 15:23:57 -0400 Subject: [PATCH 17/32] Address the second federal claimable-filer review - Education credits: a dependent student is not eligible on a return whose filer can be claimed outside the filing exception (IRC 25A(f)(1)(A)(iii), 152(b)(1)); the filer's own eligibility still turns on an actual claim. - Recovery rebates: no credit when every filer is a dependent of another taxpayer (IRC 6428B(a)); the CARES and CAA child prerequisite is a spouse with a valid SSN, whether or not that spouse is an eligible individual. - Child and dependent care: cdcc_qualifying_childcare_expenses holds the expense rule, and the single-parent work requirement reform and California's expenses read it. - References: Georgia HB 136 page 3; one entry per recovery rebate statute; quote IRC 24(h)(4)(A) exactly in the test comment. - Tests: dependent students, the two rebate cases, the reform and California expenses, and the Hawaii, Wisconsin and Treasury reforms with a claimable filer under both labellings. Co-Authored-By: Claude Opus 5.5 --- .../cdcc_single_parent_work_requirement.py | 2 +- .../ctc_adult_individual_maximum.yaml | 2 +- ...gible_for_american_opportunity_credit.yaml | 166 ++++++++++++++++++ .../recovery_rebate_credit.yaml | 96 ++++++++++ .../ca_cdcc_relevant_expenses.yaml | 61 +++++++ .../hi/dependent_exemption_reform_test.yaml | 54 ++++++ .../wi/dependent_exemption_reform_test.yaml | 54 ++++++ .../treasury/repeal_dependent_exemptions.yaml | 43 +++++ .../cdcc_single_parent_work_requirement.yaml | 57 ++++++ .../cdcc_qualifying_childcare_expenses.py | 32 ++++ .../credits/cdcc/cdcc_relevant_expenses.py | 20 +-- ...ligible_for_american_opportunity_credit.py | 8 + ...s_eligible_for_lifetime_learning_credit.py | 8 + .../rrc_adult_count_with_valid_ssn.py | 10 +- .../recovery_rebate_credit/rrc_arpa.py | 6 +- .../credits/recovery_rebate_credit/rrc_caa.py | 10 +- .../recovery_rebate_credit/rrc_cares.py | 10 +- .../credits/ca_cdcc_relevant_expenses.py | 2 +- .../credits/ctc/ga_ctc_eligible_child.py | 2 +- 19 files changed, 615 insertions(+), 28 deletions(-) create mode 100644 policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py diff --git a/policyengine_us/reforms/cdcc/cdcc_single_parent_work_requirement.py b/policyengine_us/reforms/cdcc/cdcc_single_parent_work_requirement.py index b83cb1ab43d..a84182005de 100644 --- a/policyengine_us/reforms/cdcc/cdcc_single_parent_work_requirement.py +++ b/policyengine_us/reforms/cdcc/cdcc_single_parent_work_requirement.py @@ -30,7 +30,7 @@ class cdcc_relevant_expenses(Variable): ) def formula(tax_unit, period, parameters): - expenses = tax_unit("tax_unit_childcare_expenses", period) + expenses = tax_unit("cdcc_qualifying_childcare_expenses", period) cdcc_limit = tax_unit("cdcc_limit", period) eligible_capped_expenses = min_(expenses, cdcc_limit) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml index 9c46915a88e..79cfb1685d3 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/maximum/individual/ctc_adult_individual_maximum.yaml @@ -61,7 +61,7 @@ members: [filer, sibling] state_code: TX output: - # IRC 24(h)(4) "any dependent of the taxpayer"; IRC 152(b)(1). + # IRC 24(h)(4)(A) "each dependent of the taxpayer"; IRC 152(b)(1). ctc_adult_individual_maximum: [0, 0] - name: 2025 joint return with one claimable spouse gets no credit for other dependents (the claimable spouse is the head) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml index b932cf52198..5544205c3d5 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.yaml @@ -331,3 +331,169 @@ state_code: TX output: is_eligible_for_american_opportunity_credit: true + +# IRC 25A(f)(1)(A)(iii): a dependent's tuition counts only if the taxpayer is +# allowed a section 151 deduction for the dependent, and a return on which a filer +# can be claimed has no dependents (IRC 152(b)(1)), unless the would-be claimant +# meets the filing exception (Publication 501). +- name: 2024 dependent student of a filer who can be claimed is not eligible + period: 2024 + input: + people: + parent: + age: 45 + has_tin: true + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + student: + age: 19 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [false, false] + +- name: 2024 dependent student of a claimable filer within the filing exception is eligible + period: 2024 + input: + people: + parent: + age: 45 + has_tin: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + student: + age: 19 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [false, true] + +- name: 2024 dependent student of a filer who cannot be claimed is eligible + period: 2024 + input: + people: + parent: + age: 45 + has_tin: true + is_tax_unit_head: true + is_tax_unit_spouse: false + student: + age: 19 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [false, true] + +- name: 2024 dependent student of a joint return with one claimable spouse is not eligible (the claimable spouse is the head) + period: 2024 + input: + people: + person_a: + age: 45 + has_tin: true + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 45 + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: true + student: + age: 19 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, student] + filing_status: JOINT + households: + household: + members: [person_a, person_b, student] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [false, false, false] + +- name: 2024 dependent student of a joint return with one claimable spouse is not eligible (the claimable spouse is the spouse) + period: 2024 + input: + people: + person_b: + age: 45 + has_tin: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 45 + has_tin: true + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + student: + age: 19 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, student] + filing_status: JOINT + households: + household: + members: [person_b, person_a, student] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [false, false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml index 2aa4b19bdf1..a1e77b517ef 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml @@ -330,3 +330,99 @@ state_code: TX output: rrc_arpa: 0 + +# IRC 6428B(a) allows the credit to an eligible individual, so a return whose only +# filer is a dependent of another taxpayer gets none, even when the filing +# exception lets that filer claim the child. +- name: 2021 claimable single filer within the filing exception gets no ARPA rebate for the child + period: 2021 + input: + people: + parent: + age: 30 + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + rrc_arpa: 0 + +# IRC 6428(g)(1)(C) and 6428A(g)(3): children count when a spouse has a valid SSN, +# whether or not that spouse is an eligible individual. Here the spouse with the +# SSN can be claimed (within the filing exception) and serves in the Armed +# Forces; the other spouse has no SSN. +- name: 2020 child payments need a spouse with an SSN, not an eligible spouse with one (the claimable spouse is the head) + period: 2020 + input: + people: + person_a: + age: 30 + is_military: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + meets_eitc_identification_requirements: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # One adult payment under the Armed Forces exception plus the child's. + rrc_cares: 1700 + rrc_caa: 1200 + +- name: 2020 child payments need a spouse with an SSN, not an eligible spouse with one (the claimable spouse is the spouse) + period: 2020 + input: + people: + person_b: + age: 30 + meets_eitc_identification_requirements: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_military: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + rrc_cares: 1700 + rrc_caa: 1200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/credits/child_dependent_care/ca_cdcc_relevant_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/credits/child_dependent_care/ca_cdcc_relevant_expenses.yaml index 41bb2e8353e..e415e951776 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/credits/child_dependent_care/ca_cdcc_relevant_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/credits/child_dependent_care/ca_cdcc_relevant_expenses.yaml @@ -117,3 +117,64 @@ dependent_care_assistance_exclusion: 4_000 # min(10_000 expenses, $6,000 two-child cap - $4,000 exclusion, 38_000) = 2_000. ca_cdcc_relevant_expenses: 2_000 + +# FTB 3506 counts expenses for qualifying persons. The child of a filer who can be +# claimed as a dependent is not one (IRC 152(b)(1)); the incapacitated relative is. +- name: 2025 childcare for the child of a claimable filer does not count + period: 2025 + input: + people: + parent: + age: 30 + employment_income: 5000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + relative: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child, relative] + tax_unit_childcare_expenses: 3000 + households: + household: + members: [parent, child, relative] + state_code: CA + output: + ca_cdcc_relevant_expenses: 0 + +- name: 2025 the same household with a filer who cannot be claimed counts the childcare + period: 2025 + input: + people: + parent: + age: 30 + employment_income: 5000 + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 5 + is_tax_unit_head: false + is_tax_unit_spouse: false + relative: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child, relative] + tax_unit_childcare_expenses: 3000 + households: + household: + members: [parent, child, relative] + state_code: CA + output: + ca_cdcc_relevant_expenses: 3000 diff --git a/policyengine_us/tests/policy/contrib/states/hi/dependent_exemption_reform_test.yaml b/policyengine_us/tests/policy/contrib/states/hi/dependent_exemption_reform_test.yaml index 8a11f290155..398865a7a4d 100644 --- a/policyengine_us/tests/policy/contrib/states/hi/dependent_exemption_reform_test.yaml +++ b/policyengine_us/tests/policy/contrib/states/hi/dependent_exemption_reform_test.yaml @@ -150,3 +150,57 @@ # exemptions from 3,432 to 1,144, widening taxable income and raising HI # income tax from 2,855.97 to 3,029.86 (about +173.89). output: {hi_income_tax: 3029.86} + +# As in the baseline, a filer who can be claimed as a dependent has no exemption of +# their own and, outside the claimant filing exception, no dependents. +- name: HI dependent exemption reform - claimable single filer has no exemptions + period: 2025 + reforms: policyengine_us.reforms.states.hi.dependent_exemption.hi_dependent_exemption_reform.hi_dependent_exemption_reform + input: + gov.contrib.states.hi.dependent_exemption.in_effect: true + people: + parent: {age: 40, employment_income: 60_000, claimable_as_dependent_on_another_return: true} + child1: {age: 10, is_tax_unit_dependent: true} + child2: {age: 12, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [parent, child1, child2], filing_status: SINGLE}} + households: {household: {members: [parent, child1, child2], state_name: HI}} + output: {hi_regular_exemptions: 0} + +- name: HI dependent exemption reform - the filing exception keeps the dependents only + period: 2025 + reforms: policyengine_us.reforms.states.hi.dependent_exemption.hi_dependent_exemption_reform.hi_dependent_exemption_reform + input: + gov.contrib.states.hi.dependent_exemption.in_effect: true + people: + parent: {age: 40, employment_income: 60_000, claimable_as_dependent_on_another_return: true, dependent_claimant_filing_exception: true} + child1: {age: 10, is_tax_unit_dependent: true} + child2: {age: 12, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [parent, child1, child2], filing_status: SINGLE}} + households: {household: {members: [parent, child1, child2], state_name: HI}} + output: {hi_regular_exemptions: 2_288} + +- name: HI dependent exemption reform - joint return with one claimable spouse keeps the other spouse only (the claimable spouse is the head) + period: 2025 + reforms: policyengine_us.reforms.states.hi.dependent_exemption.hi_dependent_exemption_reform.hi_dependent_exemption_reform + input: + gov.contrib.states.hi.dependent_exemption.in_effect: true + people: + person_a: {age: 40, claimable_as_dependent_on_another_return: true, is_tax_unit_head: true, is_tax_unit_spouse: false} + person_b: {age: 40, employment_income: 60_000, is_tax_unit_head: false, is_tax_unit_spouse: true} + child1: {age: 10, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [person_a, person_b, child1], filing_status: JOINT}} + households: {household: {members: [person_a, person_b, child1], state_name: HI}} + output: {hi_regular_exemptions: 1_144} + +- name: HI dependent exemption reform - joint return with one claimable spouse keeps the other spouse only (the claimable spouse is the spouse) + period: 2025 + reforms: policyengine_us.reforms.states.hi.dependent_exemption.hi_dependent_exemption_reform.hi_dependent_exemption_reform + input: + gov.contrib.states.hi.dependent_exemption.in_effect: true + people: + person_b: {age: 40, employment_income: 60_000, is_tax_unit_head: true, is_tax_unit_spouse: false} + person_a: {age: 40, claimable_as_dependent_on_another_return: true, is_tax_unit_head: false, is_tax_unit_spouse: true} + child1: {age: 10, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [person_b, person_a, child1], filing_status: JOINT}} + households: {household: {members: [person_b, person_a, child1], state_name: HI}} + output: {hi_regular_exemptions: 1_144} diff --git a/policyengine_us/tests/policy/contrib/states/wi/dependent_exemption_reform_test.yaml b/policyengine_us/tests/policy/contrib/states/wi/dependent_exemption_reform_test.yaml index 05709219f00..c6cfa3a680a 100644 --- a/policyengine_us/tests/policy/contrib/states/wi/dependent_exemption_reform_test.yaml +++ b/policyengine_us/tests/policy/contrib/states/wi/dependent_exemption_reform_test.yaml @@ -137,3 +137,57 @@ # 50,594. Wis. Stat. 71.06(1r): 3.5% x 14,680 + 4.4% x (50,480 - 14,680) # + 5.3% x (50,594 - 50,480) = 2,095.04 (baseline 2,032.42). output: {wi_income_tax: 2095.04} + +# As in the baseline, a filer who can be claimed as a dependent has no exemption of +# their own and, outside the claimant filing exception, no dependents. +- name: WI dependent exemption reform - claimable single filer has no exemptions + period: 2025 + reforms: policyengine_us.reforms.states.wi.dependent_exemption.wi_dependent_exemption_reform.wi_dependent_exemption_reform + input: + gov.contrib.states.wi.dependent_exemption.in_effect: true + people: + parent: {age: 40, employment_income: 60_000, claimable_as_dependent_on_another_return: true} + child1: {age: 10, is_tax_unit_dependent: true} + child2: {age: 12, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [parent, child1, child2], filing_status: SINGLE}} + households: {household: {members: [parent, child1, child2], state_name: WI}} + output: {wi_base_exemption: 0} + +- name: WI dependent exemption reform - the filing exception keeps the dependents only + period: 2025 + reforms: policyengine_us.reforms.states.wi.dependent_exemption.wi_dependent_exemption_reform.wi_dependent_exemption_reform + input: + gov.contrib.states.wi.dependent_exemption.in_effect: true + people: + parent: {age: 40, employment_income: 60_000, claimable_as_dependent_on_another_return: true, dependent_claimant_filing_exception: true} + child1: {age: 10, is_tax_unit_dependent: true} + child2: {age: 12, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [parent, child1, child2], filing_status: SINGLE}} + households: {household: {members: [parent, child1, child2], state_name: WI}} + output: {wi_base_exemption: 1_400} + +- name: WI dependent exemption reform - joint return with one claimable spouse keeps the other spouse only (the claimable spouse is the head) + period: 2025 + reforms: policyengine_us.reforms.states.wi.dependent_exemption.wi_dependent_exemption_reform.wi_dependent_exemption_reform + input: + gov.contrib.states.wi.dependent_exemption.in_effect: true + people: + person_a: {age: 40, claimable_as_dependent_on_another_return: true, is_tax_unit_head: true, is_tax_unit_spouse: false} + person_b: {age: 40, employment_income: 60_000, is_tax_unit_head: false, is_tax_unit_spouse: true} + child1: {age: 10, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [person_a, person_b, child1], filing_status: JOINT}} + households: {household: {members: [person_a, person_b, child1], state_name: WI}} + output: {wi_base_exemption: 700} + +- name: WI dependent exemption reform - joint return with one claimable spouse keeps the other spouse only (the claimable spouse is the spouse) + period: 2025 + reforms: policyengine_us.reforms.states.wi.dependent_exemption.wi_dependent_exemption_reform.wi_dependent_exemption_reform + input: + gov.contrib.states.wi.dependent_exemption.in_effect: true + people: + person_b: {age: 40, employment_income: 60_000, is_tax_unit_head: true, is_tax_unit_spouse: false} + person_a: {age: 40, claimable_as_dependent_on_another_return: true, is_tax_unit_head: false, is_tax_unit_spouse: true} + child1: {age: 10, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [person_b, person_a, child1], filing_status: JOINT}} + households: {household: {members: [person_b, person_a, child1], state_name: WI}} + output: {wi_base_exemption: 700} diff --git a/policyengine_us/tests/policy/contrib/treasury/repeal_dependent_exemptions.yaml b/policyengine_us/tests/policy/contrib/treasury/repeal_dependent_exemptions.yaml index ec4d5fb575f..42c59a9621b 100644 --- a/policyengine_us/tests/policy/contrib/treasury/repeal_dependent_exemptions.yaml +++ b/policyengine_us/tests/policy/contrib/treasury/repeal_dependent_exemptions.yaml @@ -63,3 +63,46 @@ output: exemptions_count: 2 exemptions: 0 + +# The personal exemptions that remain are the filers' own, and a filer who can be +# claimed as a dependent has none (IRC 151(d)(2)). +- name: Claimable single filer with a child has no personal exemption + period: 2022 + reforms: policyengine_us.reforms.treasury.repeal_dependent_exemptions.repeal_dependent_exemptions + input: + gov.contrib.treasury.repeal_dependent_exemptions: true + people: + parent: {age: 30, claimable_as_dependent_on_another_return: true} + child: {age: 5, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [parent, child]}} + households: {household: {members: [parent, child], state_code: TX}} + output: + exemptions_count: 0 + +- name: Joint return with one claimable spouse keeps the other spouse's exemption (the claimable spouse is the head) + period: 2022 + reforms: policyengine_us.reforms.treasury.repeal_dependent_exemptions.repeal_dependent_exemptions + input: + gov.contrib.treasury.repeal_dependent_exemptions: true + people: + person_a: {age: 30, claimable_as_dependent_on_another_return: true, is_tax_unit_head: true, is_tax_unit_spouse: false} + person_b: {age: 30, is_tax_unit_head: false, is_tax_unit_spouse: true} + child: {age: 5, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [person_a, person_b, child], filing_status: JOINT}} + households: {household: {members: [person_a, person_b, child], state_code: TX}} + output: + exemptions_count: 1 + +- name: Joint return with one claimable spouse keeps the other spouse's exemption (the claimable spouse is the spouse) + period: 2022 + reforms: policyengine_us.reforms.treasury.repeal_dependent_exemptions.repeal_dependent_exemptions + input: + gov.contrib.treasury.repeal_dependent_exemptions: true + people: + person_b: {age: 30, is_tax_unit_head: true, is_tax_unit_spouse: false} + person_a: {age: 30, claimable_as_dependent_on_another_return: true, is_tax_unit_head: false, is_tax_unit_spouse: true} + child: {age: 5, is_tax_unit_dependent: true} + tax_units: {tax_unit: {members: [person_b, person_a, child], filing_status: JOINT}} + households: {household: {members: [person_b, person_a, child], state_code: TX}} + output: + exemptions_count: 1 diff --git a/policyengine_us/tests/policy/reform/cdcc_single_parent_work_requirement.yaml b/policyengine_us/tests/policy/reform/cdcc_single_parent_work_requirement.yaml index b19d62d4314..5de44b287cf 100644 --- a/policyengine_us/tests/policy/reform/cdcc_single_parent_work_requirement.yaml +++ b/policyengine_us/tests/policy/reform/cdcc_single_parent_work_requirement.yaml @@ -152,3 +152,60 @@ min_head_spouse_earned: 0 # Reform: min($8,000, $3,000, max($0, $60,000)) = min($8,000, $3,000, $60,000) = $3,000 cdcc_relevant_expenses: 3_000 + +# The reform changes the earnings test only. Childcare for the child of a filer +# who can be claimed as a dependent still does not count (IRC 21(b)(2)(A)(ii)). +- name: Reform keeps out childcare for the child of a filer who can be claimed + period: 2024 + reforms: policyengine_us.reforms.cdcc.cdcc_single_parent_work_requirement.cdcc_single_parent_work_requirement + input: + people: + adult1: + age: 35 + is_tax_unit_head: true + employment_income: 50_000 + claimable_as_dependent_on_another_return: true + child: + age: 5 + is_tax_unit_dependent: true + relative: + age: 40 + is_tax_unit_dependent: true + is_incapable_of_self_care: true + tax_units: + tax_unit: + members: [adult1, child, relative] + tax_unit_childcare_expenses: 3_000 + households: + household: + members: [adult1, child, relative] + state_code: TX + output: + cdcc_relevant_expenses: 0 + +- name: Reform counts the childcare when the filer cannot be claimed + period: 2024 + reforms: policyengine_us.reforms.cdcc.cdcc_single_parent_work_requirement.cdcc_single_parent_work_requirement + input: + people: + adult1: + age: 35 + is_tax_unit_head: true + employment_income: 50_000 + child: + age: 5 + is_tax_unit_dependent: true + relative: + age: 40 + is_tax_unit_dependent: true + is_incapable_of_self_care: true + tax_units: + tax_unit: + members: [adult1, child, relative] + tax_unit_childcare_expenses: 3_000 + households: + household: + members: [adult1, child, relative] + state_code: TX + output: + cdcc_relevant_expenses: 3_000 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py new file mode 100644 index 00000000000..2162ccd2f87 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class cdcc_qualifying_childcare_expenses(Variable): + value_type = float + entity = TaxUnit + label = "Childcare expenses that count for the child and dependent care credit" + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/21#b_2_A", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", + ) + + def formula(tax_unit, period, parameters): + childcare = tax_unit("tax_unit_childcare_expenses", period) + # IRC 21(b)(2)(A)(ii) counts only expenses "for the care of a + # qualifying individual". A return on which a filer can be claimed as + # a dependent (outside the claimant filing exception) has no + # qualifying child under 13 unless the child is incapable of + # self-care, so its childcare expenses do not count. The model records + # childcare as a tax-unit total, so it cannot leave out one child's + # share when another child qualifies. + p = parameters(period).gov.irs.credits.cdcc.eligibility + person = tax_unit.members + qualifying_child = person("is_cdcc_eligible", period) & ( + person("age", period) < p.child_age + ) + dependent_filer = tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) + return where(dependent_filer & ~tax_unit.any(qualifying_child), 0, childcare) diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py index fba13665ad0..aa020f78a20 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_relevant_expenses.py @@ -18,22 +18,10 @@ def formula(tax_unit, period, parameters): # (tax_unit_childcare_expenses) plus care for a disabled qualifying # individual of any age — a disabled adult dependent or spouse # (care_expenses, a person-level yearly input). - childcare = tax_unit("tax_unit_childcare_expenses", period) - # IRC 21(b)(2)(A)(ii) counts only expenses "for the care of a - # qualifying individual". A return on which a filer can be claimed as - # a dependent has no qualifying child under 13 unless the child is - # incapable of self-care, so its childcare expenses do not count. - p = parameters(period).gov.irs.credits.cdcc.eligibility - person = tax_unit.members - qualifying_child = person("is_cdcc_eligible", period) & ( - person("age", period) < p.child_age - ) - dependent_filer = tax_unit( - "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period - ) - childcare = where( - dependent_filer & ~tax_unit.any(qualifying_child), 0, childcare - ) + # cdcc_qualifying_childcare_expenses leaves out childcare for a child + # who is not a qualifying individual because a filer can be claimed + # as a dependent. + childcare = tax_unit("cdcc_qualifying_childcare_expenses", period) adult_care = add(tax_unit, period, ["care_expenses"]) expenses = childcare + adult_care cdcc_limit = tax_unit("cdcc_limit", period) diff --git a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py index 91dd0cdffa3..28572bd8f05 100644 --- a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py @@ -27,6 +27,13 @@ def formula(person, period, parameters): filer = person("is_tax_unit_head_or_spouse", period) claimed = person("claimed_as_dependent_on_another_return", period) filer_claimed = tax_unit.any(filer & claimed) + # IRC 25A(f)(1)(A)(iii) counts the tuition of a dependent only if the + # taxpayer "is allowed a deduction under section 151" for them, and a + # return on which a filer can be claimed as a dependent (outside the + # claimant filing exception) has no dependents (IRC 152(b)(1)). + dependent_student_barred = person("is_tax_unit_dependent", period) & tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) requires_1098_t = aoc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( person("has_american_opportunity_credit_1098_t_or_exception", period) @@ -76,4 +83,5 @@ def formula(person, period, parameters): ) & filing_status_eligible & ~filer_claimed + & ~dependent_student_barred ) diff --git a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py index 0dba195cf08..c269b7f6f96 100644 --- a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py @@ -28,6 +28,13 @@ def formula(person, period, parameters): filer = person("is_tax_unit_head_or_spouse", period) claimed = person("claimed_as_dependent_on_another_return", period) filer_claimed = tax_unit.any(filer & claimed) + # IRC 25A(f)(1)(A)(iii) counts the tuition of a dependent only if the + # taxpayer "is allowed a deduction under section 151" for them, and a + # return on which a filer can be claimed as a dependent (outside the + # claimant filing exception) has no dependents (IRC 152(b)(1)). + dependent_student_barred = person("is_tax_unit_dependent", period) & tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) requires_1098_t = llc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( person("has_lifetime_learning_credit_1098_t_or_exception", period) @@ -55,4 +62,5 @@ def formula(person, period, parameters): ) & filing_status_eligible & ~filer_claimed + & ~dependent_student_barred ) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py index 63612661479..81b908901fd 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_adult_count_with_valid_ssn.py @@ -7,12 +7,12 @@ class rrc_adult_count_with_valid_ssn(Variable): definition_period = YEAR label = "Count of tax unit head/spouse with valid SSN for RRC" reference = ( - "https://www.law.cornell.edu/uscode/text/26/6428#g_1_A", - "https://www.law.cornell.edu/uscode/text/26/6428#g_1_B", + # Subsections (d)(2) and (g)(1)(A)-(B). + "https://www.law.cornell.edu/uscode/text/26/6428#g_1", + # Subsections (d)(2) and (g)(1)-(2). "https://www.law.cornell.edu/uscode/text/26/6428A#g_1", - "https://www.law.cornell.edu/uscode/text/26/6428A#g_2", - "https://www.law.cornell.edu/uscode/text/26/6428B#e_2_A", - "https://www.law.cornell.edu/uscode/text/26/6428B#e_2_B", + # Subsections (c)(2) and (e)(2)(A)-(B). + "https://www.law.cornell.edu/uscode/text/26/6428B#e_2", ) def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py index 0ffd5e954b4..e5f35eb2829 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_arpa.py @@ -47,4 +47,8 @@ def formula(tax_unit, period, parameters): excess = max_(0, agi - rrc.arpa.phase_out.threshold[filing_status]) payment_reduction_percent = excess / phase_out_length payment_reduction = max_payment * payment_reduction_percent - return max_(0, max_payment - payment_reduction) + # IRC 6428B(a) allows the credit to "an eligible individual", so a + # return whose every filer is a dependent of another taxpayer gets + # none, even for its own dependents. + no_eligible_filer = tax_unit("every_filer_is_dependent_elsewhere", period) + return where(no_eligible_filer, 0, max_(0, max_payment - payment_reduction)) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py index b0ee925ced1..7bd14989385 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_caa.py @@ -42,7 +42,15 @@ def formula(tax_unit, period, parameters): children_with_ssn = tax_unit( "rrc_cares_qualifying_children_with_valid_ssn", period ) - count_children = where(adults_with_ssn > 0, children_with_ssn, 0) + # This prerequisite is a spouse with a valid SSN, whether or not that + # spouse is an eligible individual; the credit itself needs a filer + # who is not a dependent of another taxpayer. + filer_has_ssn = tax_unit.any( + person("is_tax_unit_head_or_spouse", period) + & person("meets_eitc_identification_requirements", period) + ) + no_eligible_filer = tax_unit("every_filer_is_dependent_elsewhere", period) + count_children = where(filer_has_ssn & ~no_eligible_filer, children_with_ssn, 0) max_payment = ( rrc.caa.max.adult * count_adults + rrc.caa.max.child * count_children ) diff --git a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py index c61fca0c9a7..20868403722 100644 --- a/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py +++ b/policyengine_us/variables/gov/irs/credits/recovery_rebate_credit/rrc_cares.py @@ -42,7 +42,15 @@ def formula(tax_unit, period, parameters): children_with_ssn = tax_unit( "rrc_cares_qualifying_children_with_valid_ssn", period ) - count_children = where(adults_with_ssn > 0, children_with_ssn, 0) + # This prerequisite is a spouse with a valid SSN, whether or not that + # spouse is an eligible individual; the credit itself needs a filer + # who is not a dependent of another taxpayer. + filer_has_ssn = tax_unit.any( + person("is_tax_unit_head_or_spouse", period) + & person("meets_eitc_identification_requirements", period) + ) + no_eligible_filer = tax_unit("every_filer_is_dependent_elsewhere", period) + count_children = where(filer_has_ssn & ~no_eligible_filer, children_with_ssn, 0) max_payment = ( rrc.cares.max.adult * count_adults + rrc.cares.max.child * count_children ) diff --git a/policyengine_us/variables/gov/states/ca/tax/income/credits/ca_cdcc_relevant_expenses.py b/policyengine_us/variables/gov/states/ca/tax/income/credits/ca_cdcc_relevant_expenses.py index 25fe3bf1736..06462eec8cc 100644 --- a/policyengine_us/variables/gov/states/ca/tax/income/credits/ca_cdcc_relevant_expenses.py +++ b/policyengine_us/variables/gov/states/ca/tax/income/credits/ca_cdcc_relevant_expenses.py @@ -20,7 +20,7 @@ def formula(tax_unit, period, parameters): # Qualifying expenses cover childcare plus care for a disabled # qualifying individual of any age (FTB 3506 instructions, # Section D). - childcare = tax_unit("tax_unit_childcare_expenses", period) + childcare = tax_unit("cdcc_qualifying_childcare_expenses", period) adult_care = add(tax_unit, period, ["care_expenses"]) expenses = childcare + adult_care # First, cap based on the number of eligible care receivers diff --git a/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py index dae5a093b99..2017b09d4b3 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/credits/ctc/ga_ctc_eligible_child.py @@ -8,7 +8,7 @@ class ga_ctc_eligible_child(Variable): definition_period = YEAR defined_for = StateCode.GA reference = ( - "https://legiscan.com/GA/text/HB136/id/3204611/Georgia-2025-HB136-Enrolled.pdf#page=2", + "https://legiscan.com/GA/text/HB136/id/3204611/Georgia-2025-HB136-Enrolled.pdf#page=3", "https://www.law.cornell.edu/uscode/text/26/24#c", ) From 3d0864f778d9e87250e717be0c0882361749a5b3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 15:51:52 -0400 Subject: [PATCH 18/32] Address the third federal claimable-filer review - Education credits: is_barred_from_education_credits_by_dependency holds the claimed-filer and dependent-student rules; both eligibility formulas and the reconciliation SSN reform's Lifetime Learning Credit read it, so the reform no longer turns a barred student's tuition into that credit. - Child and dependent care: childcare counts when any child under 18 is still a qualifying individual, so an incapacitated child aged 13 to 17 keeps it. Co-Authored-By: Claude Opus 5.5 --- .../reconciled_ssn_for_llc_and_aoc.py | 9 +- .../credits/cdcc/cdcc_relevant_expenses.yaml | 30 ++++ .../reconciled_ssn_for_llc_and_aoc.yaml | 162 ++++++++++++++++++ .../cdcc_qualifying_childcare_expenses.py | 14 +- ...ligible_for_american_opportunity_credit.py | 23 +-- ...ed_from_education_credits_by_dependency.py | 34 ++++ ...s_eligible_for_lifetime_learning_credit.py | 23 +-- 7 files changed, 253 insertions(+), 42 deletions(-) create mode 100644 policyengine_us/variables/gov/irs/credits/education/is_barred_from_education_credits_by_dependency.py diff --git a/policyengine_us/reforms/reconciliation/reconciled_ssn_for_llc_and_aoc.py b/policyengine_us/reforms/reconciliation/reconciled_ssn_for_llc_and_aoc.py index 86740f80b31..b145dbd15f7 100644 --- a/policyengine_us/reforms/reconciliation/reconciled_ssn_for_llc_and_aoc.py +++ b/policyengine_us/reforms/reconciliation/reconciled_ssn_for_llc_and_aoc.py @@ -25,8 +25,15 @@ def formula(tax_unit, period, parameters): is_aoc_eligible = person( "is_eligible_for_american_opportunity_credit", period ) + # This reform leaves the dependency rules of IRC 25A(g)(3) and + # 25A(f)(1)(A)(iii) in place. + barred_by_dependency = person( + "is_barred_from_education_credits_by_dependency", period + ) eligible_expenses = tax_unit.sum( - person("qualified_tuition_expenses", period) * ~is_aoc_eligible + person("qualified_tuition_expenses", period) + * ~is_aoc_eligible + * ~barred_by_dependency ) capped_expenses = min_(llc.expense_limit, eligible_expenses) maximum_amount = llc.rate * capped_expenses diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml index 11084acd2b8..d772de304e6 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_relevant_expenses.yaml @@ -238,3 +238,33 @@ output: count_cdcc_eligible: 1 cdcc_relevant_expenses: 3000 + +# IRC 21(b)(1)(B): a dependent incapable of self-care is a qualifying individual at +# any age, determined without regard to IRC 152(b)(1), so the childcare of an +# incapacitated 14-year-old counts on a return with a claimable filer. +- name: 2025 childcare for the incapacitated teenager of a claimable filer counts + period: 2025 + input: + people: + parent: + age: 40 + employment_income: 5000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + child: + age: 14 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, child] + tax_unit_childcare_expenses: 1000 + households: + household: + members: [parent, child] + state_code: TX + output: + count_cdcc_eligible: 1 + cdcc_relevant_expenses: 1000 diff --git a/policyengine_us/tests/policy/contrib/reconciliation/reconciled_ssn_for_llc_and_aoc.yaml b/policyengine_us/tests/policy/contrib/reconciliation/reconciled_ssn_for_llc_and_aoc.yaml index 8a61f5667d5..4740dfe0d8e 100644 --- a/policyengine_us/tests/policy/contrib/reconciliation/reconciled_ssn_for_llc_and_aoc.yaml +++ b/policyengine_us/tests/policy/contrib/reconciliation/reconciled_ssn_for_llc_and_aoc.yaml @@ -73,3 +73,165 @@ output: american_opportunity_credit: 1_000 lifetime_learning_credit: 0 + +# The reform keeps the dependency rules: a dependent student's tuition does not +# count on a return whose filer can be claimed as a dependent (IRC +# 25A(f)(1)(A)(iii), 152(b)(1)), under either credit. +- name: Dependent student of a filer who can be claimed gets neither credit + period: 2026 + reforms: policyengine_us.reforms.reconciliation.reconciled_ssn_for_llc_and_aoc.reconciled_ssn_for_llc_and_aoc + input: + gov.contrib.reconciliation.ssn_for_llc_and_aoc.in_effect: true + people: + parent: + age: 45 + ssn_card_type: CITIZEN + has_tin: true + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + student: + age: 19 + ssn_card_type: CITIZEN + has_tin: true + qualified_tuition_expenses: 4_000 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, student] + filing_status: HEAD_OF_HOUSEHOLD + households: + household: + members: [parent, student] + state_code: TX + output: + american_opportunity_credit: 0 + lifetime_learning_credit: 0 + +- name: Dependent student of a joint return with one claimable spouse gets neither credit (the claimable spouse is the head) + period: 2026 + reforms: policyengine_us.reforms.reconciliation.reconciled_ssn_for_llc_and_aoc.reconciled_ssn_for_llc_and_aoc + input: + gov.contrib.reconciliation.ssn_for_llc_and_aoc.in_effect: true + people: + person_a: + age: 45 + ssn_card_type: CITIZEN + has_tin: true + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 45 + ssn_card_type: CITIZEN + has_tin: true + is_tax_unit_head: false + is_tax_unit_spouse: true + student: + age: 19 + ssn_card_type: CITIZEN + has_tin: true + qualified_tuition_expenses: 4_000 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_a, person_b, student] + filing_status: JOINT + households: + household: + members: [person_a, person_b, student] + state_code: TX + output: + american_opportunity_credit: 0 + lifetime_learning_credit: 0 + +- name: Dependent student of a joint return with one claimable spouse gets neither credit (the claimable spouse is the spouse) + period: 2026 + reforms: policyengine_us.reforms.reconciliation.reconciled_ssn_for_llc_and_aoc.reconciled_ssn_for_llc_and_aoc + input: + gov.contrib.reconciliation.ssn_for_llc_and_aoc.in_effect: true + people: + person_b: + age: 45 + ssn_card_type: CITIZEN + has_tin: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 45 + ssn_card_type: CITIZEN + has_tin: true + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + student: + age: 19 + ssn_card_type: CITIZEN + has_tin: true + qualified_tuition_expenses: 4_000 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person_b, person_a, student] + filing_status: JOINT + households: + household: + members: [person_b, person_a, student] + state_code: TX + output: + american_opportunity_credit: 0 + lifetime_learning_credit: 0 + +- name: Dependent student of a filer who cannot be claimed is eligible for the American Opportunity Credit, not the Lifetime Learning Credit + period: 2026 + reforms: policyengine_us.reforms.reconciliation.reconciled_ssn_for_llc_and_aoc.reconciled_ssn_for_llc_and_aoc + input: + gov.contrib.reconciliation.ssn_for_llc_and_aoc.in_effect: true + people: + parent: + age: 45 + ssn_card_type: CITIZEN + has_tin: true + is_tax_unit_head: true + is_tax_unit_spouse: false + student: + age: 19 + ssn_card_type: CITIZEN + has_tin: true + qualified_tuition_expenses: 4_000 + is_pursuing_credential_for_american_opportunity_credit: true + attends_eligible_educational_institution_for_american_opportunity_credit: true + is_enrolled_at_least_half_time_for_american_opportunity_credit: true + has_american_opportunity_credit_1098_t_or_exception: true + has_american_opportunity_credit_institution_ein: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [parent, student] + filing_status: HEAD_OF_HOUSEHOLD + households: + household: + members: [parent, student] + state_code: TX + output: + is_eligible_for_american_opportunity_credit: [false, true] + lifetime_learning_credit: 0 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py index 2162ccd2f87..f7885005bbf 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_qualifying_childcare_expenses.py @@ -17,14 +17,14 @@ def formula(tax_unit, period, parameters): # IRC 21(b)(2)(A)(ii) counts only expenses "for the care of a # qualifying individual". A return on which a filer can be claimed as # a dependent (outside the claimant filing exception) has no - # qualifying child under 13 unless the child is incapable of - # self-care, so its childcare expenses do not count. The model records - # childcare as a tax-unit total, so it cannot leave out one child's - # share when another child qualifies. - p = parameters(period).gov.irs.credits.cdcc.eligibility + # qualifying child unless the child is incapable of self-care, so its + # childcare expenses do not count otherwise. Childcare expenses are + # those of the tax unit's children under 18, recorded as a tax-unit + # total, so one child's share cannot be left out when another child + # qualifies. person = tax_unit.members - qualifying_child = person("is_cdcc_eligible", period) & ( - person("age", period) < p.child_age + qualifying_child = person("is_cdcc_eligible", period) & person( + "is_child", period ) dependent_filer = tax_unit( "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period diff --git a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py index 28572bd8f05..b3d5406e196 100644 --- a/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/american_opportunity_credit/is_eligible_for_american_opportunity_credit.py @@ -18,21 +18,11 @@ def formula(person, period, parameters): filing_status = tax_unit("filing_status", period) filing_status_values = filing_status.possible_values filing_status_eligible = filing_status != filing_status_values.SEPARATE - # IRC 25A(g)(3): no credit to an individual for whom a section 151 - # deduction "is allowed to another taxpayer", and the Form 1040 - # instructions extend this to "your spouse if filing jointly", so - # either filer being claimed bars the return's credit. This turns on - # an actual claim (26 CFR 1.25A-1(f)), so it reads the claimed input - # rather than a claimability helper. - filer = person("is_tax_unit_head_or_spouse", period) - claimed = person("claimed_as_dependent_on_another_return", period) - filer_claimed = tax_unit.any(filer & claimed) - # IRC 25A(f)(1)(A)(iii) counts the tuition of a dependent only if the - # taxpayer "is allowed a deduction under section 151" for them, and a - # return on which a filer can be claimed as a dependent (outside the - # claimant filing exception) has no dependents (IRC 152(b)(1)). - dependent_student_barred = person("is_tax_unit_dependent", period) & tax_unit( - "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + # The dependency rules of IRC 25A(g)(3) and 25A(f)(1)(A)(iii): a + # claimed filer bars the return, and a dependent student needs the + # return to have dependents. + barred_by_dependency = person( + "is_barred_from_education_credits_by_dependency", period ) requires_1098_t = aoc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( @@ -82,6 +72,5 @@ def formula(person, period, parameters): period, ) & filing_status_eligible - & ~filer_claimed - & ~dependent_student_barred + & ~barred_by_dependency ) diff --git a/policyengine_us/variables/gov/irs/credits/education/is_barred_from_education_credits_by_dependency.py b/policyengine_us/variables/gov/irs/credits/education/is_barred_from_education_credits_by_dependency.py new file mode 100644 index 00000000000..6a08d71c198 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/education/is_barred_from_education_credits_by_dependency.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class is_barred_from_education_credits_by_dependency(Variable): + value_type = bool + entity = Person + label = "Barred from the education credits by the dependency rules" + definition_period = YEAR + reference = ( + # Subsections (f)(1)(A)(iii) and (g)(3). + "https://www.law.cornell.edu/uscode/text/26/25A#g_3", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", + "https://www.ecfr.gov/current/title-26/section-1.25A-1", + ) + + def formula(person, period, parameters): + tax_unit = person.tax_unit + # IRC 25A(g)(3): no credit to an individual for whom a section 151 + # deduction "is allowed to another taxpayer", and the Form 1040 + # instructions extend this to "your spouse if filing jointly", so + # either filer being claimed bars the return's credit. This turns on + # an actual claim (26 CFR 1.25A-1(f)), so it reads the claimed input + # rather than a claimability helper. + filer = person("is_tax_unit_head_or_spouse", period) + claimed = person("claimed_as_dependent_on_another_return", period) + filer_claimed = tax_unit.any(filer & claimed) + # IRC 25A(f)(1)(A)(iii) counts the tuition of a dependent only if the + # taxpayer "is allowed a deduction under section 151" for them, and a + # return on which a filer can be claimed as a dependent (outside the + # claimant filing exception) has no dependents (IRC 152(b)(1)). + dependent_student_barred = person("is_tax_unit_dependent", period) & tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) + return filer_claimed | dependent_student_barred diff --git a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py index c269b7f6f96..1a7bbf0a847 100644 --- a/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py +++ b/policyengine_us/variables/gov/irs/credits/education/is_eligible_for_lifetime_learning_credit.py @@ -19,21 +19,11 @@ def formula(person, period, parameters): filing_status = tax_unit("filing_status", period) filing_status_values = filing_status.possible_values filing_status_eligible = filing_status != filing_status_values.SEPARATE - # IRC 25A(g)(3): no credit to an individual for whom a section 151 - # deduction "is allowed to another taxpayer", and the Form 1040 - # instructions extend this to "your spouse if filing jointly", so - # either filer being claimed bars the return's credit. This turns on - # an actual claim (26 CFR 1.25A-1(f)), so it reads the claimed input - # rather than a claimability helper. - filer = person("is_tax_unit_head_or_spouse", period) - claimed = person("claimed_as_dependent_on_another_return", period) - filer_claimed = tax_unit.any(filer & claimed) - # IRC 25A(f)(1)(A)(iii) counts the tuition of a dependent only if the - # taxpayer "is allowed a deduction under section 151" for them, and a - # return on which a filer can be claimed as a dependent (outside the - # claimant filing exception) has no dependents (IRC 152(b)(1)). - dependent_student_barred = person("is_tax_unit_dependent", period) & tax_unit( - "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + # The dependency rules of IRC 25A(g)(3) and 25A(f)(1)(A)(iii): a + # claimed filer bars the return, and a dependent student needs the + # return to have dependents. + barred_by_dependency = person( + "is_barred_from_education_credits_by_dependency", period ) requires_1098_t = llc.eligibility.requires_1098_t_or_exception has_1098_t_or_exception = ( @@ -61,6 +51,5 @@ def formula(person, period, parameters): period, ) & filing_status_eligible - & ~filer_claimed - & ~dependent_student_barred + & ~barred_by_dependency ) From 35a2cbcbb971f6b43d35db009d9bda41b76386a5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 16:02:49 -0400 Subject: [PATCH 19/32] Give the spouse without an SSN a card type in the recovery rebate test Setting meets_eitc_identification_requirements on one person made it an input for every person, so the other spouse lost their SSN too. Co-Authored-By: Claude Opus 5.5 --- .../recovery_rebate_credit/recovery_rebate_credit.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml index a1e77b517ef..01ed4209665 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/recovery_rebate_credit/recovery_rebate_credit.yaml @@ -375,7 +375,7 @@ is_tax_unit_spouse: false person_b: age: 30 - meets_eitc_identification_requirements: false + ssn_card_type: NONE is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -401,7 +401,7 @@ people: person_b: age: 30 - meets_eitc_identification_requirements: false + ssn_card_type: NONE is_tax_unit_head: true is_tax_unit_spouse: false person_a: From dc66236d6f7020945a9c0574928aedeb76fc35ea Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:29:46 -0400 Subject: [PATCH 20/32] Address review r1: claimability, household income, premium cap, CSR, role-less units - is_aca_tax_family_member reads claimable_as_dependent_on_another_return (#10024's split input: 36B(c)(1)(D) and 1.36B-1(d)(2) turn on being claimable, and Form 1040 line 12a, which Form 8962 line 1 keys on, has no refund-only carve-out). The return keeps its dependents when the would-be claimant meets the refund-only filing exception (Pub 501, Dependent Taxpayer Test), a return with no applicable taxpayer has no family, and a tax unit with neither a head nor a spouse uses each member's own claimability (a lone claimable minor no longer gets the credit). - aca_magi leaves out the tax-exempt interest and Social Security of the return's dependents who are outside the tax family (26 CFR 1.36B-1(e)(1)). - used_aca_ptc is capped at the coverage family's share of the selected plan premium (new aca_ptc_coverage_family_premium; 26 CFR 1.36B-3(d)(1)(i), (h)(1)), not the whole policy's premium. - marketplace_csr_eligible applies 45 CFR 155.305(g)(3): the model prices one policy per tax unit, and a policy that also covers an enrollee without credit eligibility gets no cost-sharing reductions. - aca_fpg documents that it sizes the family for the tax year; the NJHPS exclusion is labelled an interpretation; one reference per source in is_aca_coverage_family_member. Co-Authored-By: Claude Opus 5.5 --- .../baseline/gov/aca/eligibility/aca_fpg.yaml | 14 +++--- .../aca/eligibility/aca_magi_fraction.yaml | 6 +-- ...a_ptc_below_fpl_immigration_exception.yaml | 4 +- .../aca/eligibility/aca_tax_family_size.yaml | 10 ++--- .../is_aca_coverage_family_member.yaml | 4 +- .../aca/eligibility/is_aca_ptc_eligible.yaml | 18 ++++---- .../eligibility/is_aca_tax_family_member.yaml | 14 +++--- .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 24 +++++------ .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 18 ++++---- .../aca_ptc_slcsp_family_tier_category.yaml | 16 +++---- .../aca_ptc_slcsp_family_tier_multiplier.yaml | 16 +++---- .../premium_subsidy/ca_premium_subsidy.yaml | 4 +- .../co_premium_assistance.yaml | 8 ++-- .../ct_covered_connecticut.yaml | 8 ++-- .../hic/connector_care/ma_connector_care.yaml | 8 ++-- .../ma_connector_care_member_eligible.yaml | 4 +- .../md_premium_assistance.yaml | 8 ++-- .../gov/states/nj/dobi/njhps/nj_njhps.yaml | 8 ++-- .../dobi/njhps/nj_njhps_member_eligible.yaml | 4 +- .../nm_premium_assistance.yaml | 8 ++-- .../nm_premium_assistance_mih_eligible.yaml | 8 ++-- .../vt_premium_assistance.yaml | 8 ++-- .../wa_cascade_care_savings.yaml | 8 ++-- ...wa_cascade_care_savings_member_amount.yaml | 4 +- ..._cascade_care_savings_member_eligible.yaml | 4 +- .../policy/contrib/aca/ptc_700_fpl_cliff.yaml | 4 +- .../contrib/aca/ptc_additional_bracket.yaml | 4 +- .../contrib/aca/ptc_simplified_bracket.yaml | 4 +- .../gov/aca/csr/marketplace_csr_eligible.py | 17 +++++++- .../variables/gov/aca/eligibility/aca_fpg.py | 14 +++--- .../variables/gov/aca/eligibility/aca_magi.py | 32 +++++++++++++- .../aca/eligibility/aca_tax_family_size.py | 3 +- .../is_aca_coverage_family_member.py | 5 +-- .../eligibility/is_aca_tax_family_member.py | 43 ++++++++++++------- .../ptc/aca_ptc_coverage_family_premium.py | 33 ++++++++++++++ .../variables/gov/aca/ptc/used_aca_ptc.py | 6 ++- .../gov/states/nj/dobi/njhps/nj_njhps.py | 3 +- .../nj/dobi/njhps/nj_njhps_member_eligible.py | 15 ++++--- 38 files changed, 258 insertions(+), 161 deletions(-) create mode 100644 policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml index 9d31224ee0b..8f2df037394 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -56,7 +56,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -125,7 +125,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a] @@ -149,7 +149,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -158,7 +158,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -183,7 +183,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -223,7 +223,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml index e352b8a0826..5f8e0b0d996 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -50,7 +50,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -109,7 +109,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml index fd777a8eb45..8e6b856b402 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true immigration_status: TPS is_basic_health_program_eligible: false is_tax_unit_head: true @@ -53,7 +53,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true immigration_status: TPS is_basic_health_program_eligible: false is_tax_unit_head: false diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml index 636b3ff9473..3133369a77d 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true child: age: 10 is_medicaid_eligible: false @@ -40,7 +40,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -96,7 +96,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -133,7 +133,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -142,7 +142,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml index 6064d5ac59d..cd17edc030b 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -57,7 +57,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml index 7d03af20362..519ed8c4040 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml @@ -506,7 +506,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -550,7 +550,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -576,7 +576,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a] @@ -599,7 +599,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -608,7 +608,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -665,7 +665,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -713,7 +713,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -745,7 +745,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -790,7 +790,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml index 7678861083c..7a150cdf659 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true child: age: 10 is_medicaid_eligible: false @@ -39,7 +39,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -88,7 +88,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -118,7 +118,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -127,7 +127,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -184,7 +184,7 @@ output: is_aca_tax_family_member: [true, true, true] -- name: 2025 dependent flagged as claimed elsewhere stays in the family of the return that claims them +- name: 2025 dependent another taxpayer could also claim stays in the family of the return that claims them period: 2025 input: people: @@ -196,7 +196,7 @@ is_medicare_eligible: false child: age: 10 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a, child] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 49a6222cef5..462efa51568 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -268,7 +268,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -317,7 +317,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -346,7 +346,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 tax_units: tax_unit: @@ -372,7 +372,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -382,7 +382,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -448,7 +448,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -497,7 +497,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -526,7 +526,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 tax_units: tax_unit: @@ -552,7 +552,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -562,7 +562,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -628,7 +628,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -675,7 +675,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml index 5720af34beb..51ae163bac9 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -62,7 +62,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -138,7 +138,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 tax_units: tax_unit: @@ -163,7 +163,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -173,7 +173,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -232,7 +232,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -275,7 +275,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -306,7 +306,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -349,7 +349,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml index 80cf923afc6..a2269f7968d 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml @@ -6,7 +6,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -48,7 +48,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -78,14 +78,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -149,7 +149,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -191,7 +191,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -221,14 +221,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml index e175f8f9941..427bf1091c7 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml @@ -6,7 +6,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -48,7 +48,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -78,14 +78,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -114,7 +114,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -156,7 +156,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -186,14 +186,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 34561143831..78d17264213 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -518,7 +518,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -570,7 +570,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml index fdd1995fa79..0df47cd45e4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml @@ -695,7 +695,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -748,7 +748,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -816,7 +816,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -827,7 +827,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml index 22c48b87423..3fd48f2227c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml @@ -385,7 +385,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -438,7 +438,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -506,7 +506,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -517,7 +517,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml index b52405c1e14..b254072224a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml @@ -580,7 +580,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -633,7 +633,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -701,7 +701,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -712,7 +712,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml index 27c4ffd05fa..80802d19164 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml @@ -70,7 +70,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -118,7 +118,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml index fe6090f2e5b..6ab68a2a44b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml @@ -737,7 +737,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -790,7 +790,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -858,7 +858,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -869,7 +869,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml index 9982d762b4a..dda7665f27a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml @@ -806,7 +806,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -859,7 +859,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -927,7 +927,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -938,7 +938,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml index a17d1e77e50..095bbeaa877 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml @@ -280,7 +280,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -328,7 +328,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml index 3b634cb0983..a0238c6b2ba 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml @@ -899,7 +899,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -952,7 +952,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -1020,7 +1020,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -1031,7 +1031,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml index 4a86773a91f..60f13561961 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml @@ -251,7 +251,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -303,7 +303,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -333,7 +333,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -344,7 +344,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml index 0623d929169..1ae052648d3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml @@ -419,7 +419,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -470,7 +470,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -537,7 +537,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -547,7 +547,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml index 5d2dd12d03d..062ef7cd7cd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml @@ -763,7 +763,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -817,7 +817,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -885,7 +885,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -896,7 +896,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml index 2caf6ea14e0..73694ff8b65 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml @@ -167,7 +167,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -216,7 +216,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml index e6343dfebfc..c18970e2535 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml @@ -186,7 +186,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -234,7 +234,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml index 737cdc45428..11dd41d0214 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml @@ -311,7 +311,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -356,7 +356,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml index ce8da297e6e..8c568ee903e 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml @@ -128,7 +128,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -173,7 +173,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml index 820a2cb1b42..cd3d6932461 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml @@ -128,7 +128,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -173,7 +173,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py index 837491802ee..5d6718c052a 100644 --- a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py +++ b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py @@ -6,15 +6,28 @@ class marketplace_csr_eligible(Variable): entity = TaxUnit label = "Tax unit is eligible for ordinary Marketplace cost-sharing reductions" definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/45/155.305#g_1" + reference = "https://www.law.cornell.edu/cfr/text/45/155.305#g" + documentation = ( + "The model prices one Marketplace policy per tax unit. Under 45 CFR " + "155.305(g)(3), a policy covering people who would be eligible for " + "different cost-sharing reductions gets only the category all of " + "them qualify for, so a policy that also covers an enrollee without " + "premium tax credit eligibility (one outside the tax family, such as " + "a spouse who can be claimed on another return) gets none." + ) def formula(tax_unit, period, parameters): person = tax_unit.members receives_aca = tax_unit.sum(person("person_receives_aca", period)) > 0 + enrollee = person("pays_aca_premium", period) + ptc_eligible = person("is_aca_ptc_eligible", period) + every_enrollee_eligible = ~tax_unit.any(enrollee & ~ptc_eligible) category = tax_unit("selected_marketplace_plan_category", period) silver_selected = category == category.possible_values.SILVER magi_fraction = tax_unit("aca_magi_fraction", period) income_eligible = ( magi_fraction <= parameters(period).gov.aca.csr.income_threshold.maximum ) - return receives_aca & silver_selected & income_eligible + return ( + receives_aca & every_enrollee_eligible & silver_selected & income_eligible + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py index 2a882f1a066..0ba7f9a9adc 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py @@ -15,11 +15,15 @@ class aca_fpg(Variable): ) documentation = ( "Form 8962 line 4: the prior year's poverty guideline for the tax " - "family size on line 1 (aca_tax_family_size). A return on which " - "every head and spouse can be claimed elsewhere has no tax family and " - "no applicable taxpayer; it keeps the guideline for the whole tax " - "unit, so readers that are not about the credit see the same value " - "as before." + "year's tax family size on line 1 (aca_tax_family_size). The former " + "lookup, tax_unit_fpg for the prior year, also took the household " + "size from the prior year; the size now comes from the tax year, as " + "line 1 requires, which differs only when a prior-year size is " + "supplied. A return on which every head and spouse can be claimed " + "elsewhere has no tax family and no applicable taxpayer; as a " + "numerical convention, not a legal rule, it keeps the guideline for " + "the whole tax unit, so readers that are not about the credit see " + "the same value as before." ) def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py index 2b9764bf6af..bdd10aa10ac 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py @@ -6,4 +6,34 @@ class aca_magi(Variable): entity = TaxUnit label = "ACA-related modified AGI for this tax unit" definition_period = YEAR - adds = ["medicaid_magi"] + unit = USD + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#d_2", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#e_1", + ) + documentation = ( + "Household income for the premium tax credit (IRC 36B(d)(2), 26 CFR " + "1.36B-1(e)(1)): the taxpayer's modified AGI, both spouses' on a " + "joint return, plus that of other members of the tax family. It is " + "medicaid_magi without the tax-exempt interest and Social Security " + "of the return's dependents who are outside the tax family " + "(is_aca_tax_family_member), which happens only on a return whose " + "head or spouse can be claimed elsewhere. medicaid_magi adds those " + "two amounts for every member (tax unit AGI already leaves " + "dependents' income out); both are nonnegative, so removing them " + "after medicaid_magi's zero floor gives the same result as removing " + "them before it." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + filer = person("is_tax_unit_head_or_spouse", period) + outside_family = ~filer & ~person("is_aca_tax_family_member", period) + excluded = tax_unit.sum( + outside_family + * ( + person("tax_exempt_interest_income", period) + + person("social_security", period) + ) + ) + return max_(0, tax_unit("medicaid_magi", period) - excluded) diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py index d8cea21ac2d..96889d8b9ec 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py @@ -15,7 +15,8 @@ class aca_tax_family_size(Variable): "Form 8962 line 1: the number of people in the premium tax credit " "tax family. It equals the tax unit size unless a head or spouse can " "be claimed as a dependent on another return, and is 0 when every " - "head and spouse can be. The shared tax_unit_size is unchanged." + "head and spouse can be (no applicable taxpayer). The shared " + "tax_unit_size is unchanged." ) adds = ["is_aca_tax_family_member"] diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py index 022482995d0..389b06a6a8c 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py @@ -6,10 +6,7 @@ class is_aca_coverage_family_member(Variable): entity = Person label = "Member of the premium tax credit coverage family" definition_period = YEAR - reference = ( - "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2", - "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f_1", - ) + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2" documentation = ( "A Marketplace enrollee (pays_aca_premium) who is in the premium tax " "credit tax family. 26 CFR 1.36B-3(b)(2) defines the coverage family " diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py index ea43bed46d3..cac4db03b6c 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -12,26 +12,37 @@ class is_aca_tax_family_member(Variable): "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d_2", "https://www.law.cornell.edu/cfr/text/26/1.36B-2#b_3", "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", ) documentation = ( - "Whether the person is in the taxpayer's family for the premium tax " - "credit (Form 8962 line 1, tax family size). 26 CFR 1.36B-1(d)(2) " - "defines the family as the taxpayer, both spouses on a joint return, " - "'except for individuals who qualify as a dependent of another " - "taxpayer', plus the dependents the taxpayer claims. A head or spouse " - "who can be claimed on another return is therefore not a member " - "(IRC 36B(c)(1)(D), 26 CFR 1.36B-2(b)(3)), and on a joint return the " - "other spouse still is. A return with a filer who can be claimed " - "elsewhere claims no dependents (IRC 152(b)(1)), so its dependents " - "are members only when no head or spouse can be claimed elsewhere. " - "The claimed flag of a dependent on this return does not matter: this " - "return claims them." + "Whether the person is in an applicable taxpayer's family for the " + "premium tax credit (Form 8962 line 1, tax family size). 26 CFR " + "1.36B-1(d)(2) defines the family as the taxpayer, both spouses on a " + "joint return, 'except for individuals who qualify as a dependent of " + "another taxpayer', plus the dependents the taxpayer claims. A head or " + "spouse who can be claimed on another return " + "(claimable_as_dependent_on_another_return, whether or not claimed) " + "is not a member; on a joint return the other spouse still is. Such a " + "return claims no dependents (IRC 152(b)(1)) unless the would-be " + "claimant meets the refund-only filing exception (Publication 501, " + "Dependent Taxpayer Test). No credit is allowed to someone another " + "taxpayer can claim (IRC 36B(c)(1)(D)), so a return whose every head " + "and spouse can be claimed has no applicable taxpayer and no family " + "for the credit. A tax unit with neither a head nor a spouse has no " + "modeled filer, so each member's own claimability decides. A " + "dependent on a return with an applicable taxpayer is a member even if " + "another taxpayer could also claim them: this return claims them." ) def formula(person, period, parameters): filer = person("is_tax_unit_head_or_spouse", period) - claimed_elsewhere = person("claimed_as_dependent_on_another_return", period) - return_has_claimed_filer = person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + claimable = person("claimable_as_dependent_on_another_return", period) + has_filer = person.tax_unit.any(filer) + applicable_taxpayer = ( + person.tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 ) - return where(filer, ~claimed_elsewhere, ~return_has_claimed_filer) + claims_dependents = ~person.tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) + dependent_member = applicable_taxpayer & claims_dependents + return where(has_filer, where(filer, ~claimable, dependent_member), ~claimable) diff --git a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py new file mode 100644 index 00000000000..d06c3482cf2 --- /dev/null +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class aca_ptc_coverage_family_premium(Variable): + value_type = float + entity = TaxUnit + label = "Selected Marketplace plan premium allocated to the coverage family" + unit = USD + definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#d_1" + documentation = ( + "The enrollment premiums for the premium tax credit's lesser-of test " + "(26 CFR 1.36B-3(d)(1)(i)): the selected plan premium for the tax " + "unit, of which the coverage family's share is its benchmark " + "(aca_ptc_slcsp) over the whole enrollment's benchmark (slcsp). 26 " + "CFR 1.36B-3(h)(1) allocates a policy covering more than one family " + "'in proportion to the premiums for each taxpayer's applicable " + "benchmark plan'; the whole-enrollment benchmark stands in for the " + "sum of those benchmarks. The share is 1 whenever every enrollee is " + "in the coverage family. It applies to bronze plans too." + ) + + def formula(tax_unit, period, parameters): + premium = tax_unit("selected_marketplace_plan_premium_proxy", period) + benchmark = add(tax_unit, period, ["slcsp"]) + coverage_family_benchmark = add(tax_unit, period, ["aca_ptc_slcsp"]) + share = np.divide( + coverage_family_benchmark, + benchmark, + out=np.ones_like(benchmark), + where=benchmark > 0, + ) + return premium * share diff --git a/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py b/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py index 6de35a53b29..da994b651b4 100644 --- a/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py +++ b/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py @@ -7,9 +7,13 @@ class used_aca_ptc(Variable): label = "Used ACA premium tax credit" unit = USD definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#d_1" def formula(tax_unit, period, parameters): + # The credit cannot exceed the enrollment premiums of the coverage + # family (26 CFR 1.36B-3(d)(1)(i)); an enrollee outside the tax family + # pays their own share of the policy. return min_( tax_unit("aca_ptc", period), - tax_unit("selected_marketplace_plan_premium_proxy", period), + tax_unit("aca_ptc_coverage_family_premium", period), ) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py index 97c660c1ad8..14c442961c6 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py @@ -50,7 +50,8 @@ def formula(tax_unit, period, parameters): # premium below zero, so it is capped at the post-APTC residual. In the # 400-600% band aca_ptc is 0, so the residual equals slcsp_annual. # The benchmark is the federal coverage family's: NJHPS members must - # be in a tax household otherwise eligible for APTC. + # be in a tax household otherwise eligible for APTC (an + # interpretation; see nj_njhps_member_eligible). slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) residual = max_(0, slcsp_annual - aca_ptc) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py index f46e8605147..2578759eebe 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py @@ -14,8 +14,10 @@ class nj_njhps_member_eligible(Variable): documentation = ( "A person is a New Jersey Health Plan Savings enrollee when they pay an " "ACA marketplace premium, are in the federal premium tax credit tax " - "family (NJHPS goes to enrollees otherwise eligible for APTC, so not " - "someone another taxpayer can claim), do not file a separate return, " + "family (an interpretation: Treasury's 1332 addendum describes NJHPS " + "as for enrollees 'otherwise eligible for the Advanced Premium Tax " + "Credit', and no current New Jersey rule found addresses someone " + "another taxpayer can claim), do not file a separate return, " "and household " "income (ACA MAGI per 26 U.S.C. 36B(d)(2)) falls within the 138-600% " "FPL NJHPS band. This deliberately does NOT reuse is_aca_ptc_eligible: " @@ -48,10 +50,11 @@ def formula(person, period, parameters): # income under the federal PTC floor of 100% FPL, and within_band # already starts at the 138% FPL NJHPS floor. pays_premium = person("pays_aca_premium", period) - # NJHPS goes to enrollees otherwise eligible for APTC, so a person - # outside the federal tax family (one who can be claimed on another - # return, or a dependent of a return with such a filer) is not a - # member (26 U.S.C. 36B(c)(1)(D) is not an income test). + # Interpretation: NJHPS goes to enrollees otherwise eligible for APTC + # (Treasury 1332 addendum), so a person outside the federal tax family + # (one who can be claimed on another return, or a dependent of a + # return with such a filer) is not a member; 26 U.S.C. 36B(c)(1)(D) + # is not one of the income tests NJHPS replaces. tax_family_member = person("is_aca_tax_family_member", period) fstatus = person.tax_unit("filing_status", period) not_separate = fstatus != fstatus.possible_values.SEPARATE From 44bfb6df63e54762c603a0f7334b05a71e33291f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:41:08 -0400 Subject: [PATCH 21/32] Test the review r1 fixes and tighten the invariant test YAML: refund-only exception (family keeps the child), single claimable filer with the exception (no family), claimable but not claimed, lone claimable and unclaimed 17-year-old filers, household income without an out-of-family child's tax-exempt interest, the coverage-family premium cap (silver and bronze), CSR under the family-policy rule, Colorado's binding residual ($420), and the tax year's family size against a supplied prior-year size. Test inputs now use claimable_as_dependent_on_another_return. Property test: Hypothesis runs every year 2021-2026 and draws claimable-but- unclaimed spouses and the filing exception; the credit's monotonicity exemption is limited to crossing the 100% floor or dependents' income leaving household income; the benchmark check compares enrolled people, not counts; new identities for household income, the premium cap and CSR. Co-Authored-By: Claude Opus 5.5 --- .../test_claimable_filer_ptc_invariants.py | 150 +++++++---- .../gov/aca/csr/marketplace_csr_eligible.yaml | 111 ++++++++ .../baseline/gov/aca/eligibility/aca_fpg.yaml | 42 +++ .../gov/aca/eligibility/aca_magi.yaml | 212 +++++++++++++++ .../aca/eligibility/aca_tax_family_size.yaml | 92 +++++++ .../eligibility/is_aca_tax_family_member.yaml | 224 ++++++++++++++++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 246 ++++++++++++++++++ .../ptc/aca_ptc_coverage_family_premium.yaml | 195 ++++++++++++++ .../gov/aca/ptc/marketplace_net_premium.yaml | 77 ++++++ .../baseline/gov/aca/ptc/used_aca_ptc.yaml | 79 ++++++ .../co_premium_assistance.yaml | 83 ++++++ 11 files changed, 1463 insertions(+), 48 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py index c0ddc4c9fc2..afde18c3e02 100644 --- a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -1,28 +1,37 @@ """The premium tax credit for a filer who can be claimed as a dependent. -Someone another taxpayer can claim is not an applicable taxpayer (IRC -36B(c)(1)(D)) and is outside the tax family (26 CFR 1.36B-1(d)(2)); a return -with such a filer claims no dependents (IRC 152(b)(1)). The credit therefore -uses `aca_tax_family_size` for the poverty line and the coverage family's -benchmark `aca_ptc_slcsp`, and the state premium programs that wrap it follow. +No credit is allowed to someone another taxpayer can claim (IRC 36B(c)(1)(D)); +such a spouse is outside the tax family (26 CFR 1.36B-1(d)(2)), and a return +with such a filer claims no dependents (IRC 152(b)(1)) unless the would-be +claimant meets the refund-only filing exception. The credit therefore uses +`aca_tax_family_size` for the poverty line, household income without +out-of-family dependents (`aca_magi`), the coverage family's benchmark +(`aca_ptc_slcsp`) and its share of the premium +(`aca_ptc_coverage_family_premium`), and the state premium programs that wrap +it follow. -For couples drawn by Hypothesis and a seeded population, with either, both or -neither spouse claimed: +For couples drawn by Hypothesis in every year 2021-2026 (spouses claimable, +actually claimed or neither, with or without the filing exception) and a +seeded population in 2025 and 2026: 1. Swap invariance: each output is the same under either head/spouse labelling. 2. Identities: `aca_tax_family_size` equals the filers who cannot be claimed - plus the dependents when no filer can be claimed; nobody outside the tax - family is eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; a return where every - filer can be claimed has no credit; and a return where no filer can be - claimed keeps `slcsp`, the tax unit size and the tax unit's poverty line. + plus the dependents when one of them is an applicable taxpayer and no + filer is claimable outside the exception; nobody outside the tax family is + eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; `aca_magi` <= `medicaid_magi`; + used credit <= coverage-family premium <= selected premium; cost-sharing + reductions only when every enrollee is eligible for the credit; no family + member means no credit; and where no filer can be claimed, `aca_ptc_slcsp` + = `slcsp`, `aca_magi` = `medicaid_magi`, `aca_tax_family_size` = + `tax_unit_size` and `aca_fpg` = `tax_unit_fpg` for the prior year (these + simulations supply no prior-year size, so the two sizes agree). 3. Monotonicity: marking another filer as claimed never raises the tax family - size, never raises the benchmark while the enrollees stay the same, and - never raises the credit of a return that had an eligible member. Both can - rise otherwise, as intended: a couple below 100% of the two-person line can - be above it for a tax family of one, so IRC 36B allows the credit and the - unclaimable spouse leaves the coverage gap that pays_aca_premium assumes - nobody buys into. + size, never raises the benchmark while the same people are enrolled, and + never raises the credit, except in two intended cases: income crosses the + 100% floor (below it for two, above it for one, so IRC 36B allows a credit + that was not allowed), or out-of-family dependents' income leaves household + income. """ import numpy as np @@ -51,10 +60,13 @@ FEDERAL = [ "aca_tax_family_size", "aca_fpg", + "aca_magi", "aca_magi_fraction", "slcsp", "aca_ptc_slcsp", "aca_ptc", + "aca_ptc_coverage_family_premium", + "used_aca_ptc", "marketplace_csr_eligible", ] STATE_WRAPS = [ @@ -73,11 +85,16 @@ @st.composite def couples(draw): + claimed = draw(st.sampled_from(CLAIM_PATTERNS)) + # Claimable whenever claimed, and sometimes claimable but not claimed. + extra = draw(st.sampled_from(CLAIM_PATTERNS)) return { "state": draw(st.sampled_from(STATES)), "adults": [draw(adults()), draw(adults())], "dependents": draw(st.lists(dependents(), max_size=2)), - "claimed": draw(st.sampled_from(CLAIM_PATTERNS)), + "claimed": claimed, + "claimable": (claimed[0] or extra[0], claimed[1] or extra[1]), + "exception": draw(st.sampled_from(CLAIM_PATTERNS)), } @@ -109,13 +126,37 @@ def _tax_unit_sum(sim, name, year): return np.asarray(sim.calculate(name, year, map_to="tax_unit"), dtype=float) +def _tax_unit_sum_array(sim, values): + tax_units = sim.populations["tax_unit"] + return np.bincount( + np.asarray(tax_units.members_entity_id), + weights=np.asarray(values, dtype=float), + minlength=tax_units.count, + ) + + +def _enrollment_signature(sim, year): + # Which members are enrolled, as one number per tax unit, so variants + # can be compared person by person rather than by count. + position = np.asarray(sim.populations["tax_unit"].members_position) + enrolled = np.asarray(sim.calculate("pays_aca_premium", year), dtype=float) + return _tax_unit_sum_array(sim, enrolled * 2.0**position) + + def _identities(sim, year): - claimed_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + claimable_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + blocked = ( + _calc( + sim, "head_or_spouse_is_dependent_elsewhere_without_filing_exception", year + ) + > 0 + ) independent = _calc(sim, "head_spouse_count_not_dependent_elsewhere", year) dependents_count = _calc(sim, "tax_unit_count_dependents", year) size = _calc(sim, "aca_tax_family_size", year) + claims_dependents = (independent > 0) & ~blocked np.testing.assert_array_equal( - size, independent + np.where(claimed_filer, 0, dependents_count) + size, independent + np.where(claims_dependents, dependents_count, 0) ) eligible = np.asarray(sim.calculate("is_aca_ptc_eligible", year)) member = np.asarray(sim.calculate("is_aca_tax_family_member", year)) @@ -124,10 +165,24 @@ def _identities(sim, year): benchmark = _calc(sim, "aca_ptc_slcsp", year) assert (benchmark >= -TOLERANCE).all() assert (benchmark <= slcsp + TOLERANCE).all(), "benchmark above slcsp" - nobody_in_family = size == 0 - assert not _calc(sim, "aca_ptc", year)[nobody_in_family].any() - unclaimed = ~claimed_filer + aca_magi = _calc(sim, "aca_magi", year) + medicaid_magi = _calc(sim, "medicaid_magi", year) + assert (aca_magi <= medicaid_magi + TOLERANCE).all() + premium = _calc(sim, "selected_marketplace_plan_premium_proxy", year) + family_premium = _calc(sim, "aca_ptc_coverage_family_premium", year) + used = _calc(sim, "used_aca_ptc", year) + assert (used <= family_premium + TOLERANCE).all() + assert (family_premium <= premium + TOLERANCE).all() + csr = _calc(sim, "marketplace_csr_eligible", year) > 0 + enrolled = np.asarray(sim.calculate("pays_aca_premium", year)) + ineligible_enrollees = _tax_unit_sum_array(sim, enrolled & ~eligible) + assert not (csr & (ineligible_enrollees > 0)).any() + assert not _calc(sim, "aca_ptc", year)[size == 0].any() + unclaimed = ~claimable_filer np.testing.assert_allclose(benchmark[unclaimed], slcsp[unclaimed], atol=TOLERANCE) + np.testing.assert_allclose( + aca_magi[unclaimed], medicaid_magi[unclaimed], atol=TOLERANCE + ) np.testing.assert_array_equal( size[unclaimed], _calc(sim, "tax_unit_size", year)[unclaimed] ) @@ -143,13 +198,13 @@ def check_swap(units, year, outputs, require_coverage=False): # The batch must reach the rule: returns with exactly one claimable # filer that still get a credit and, from 2026 when the modeled state # programs start, a state wrap paid on one. - one_claimed = ( + one_claimable = ( _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 ) & (_calc(sim, "head_spouse_count_not_dependent_elsewhere", year) == 1) - assert (_calc(sim, "aca_ptc", year)[one_claimed] > 0).any() + assert (_calc(sim, "aca_ptc", year)[one_claimable] > 0).any() if year >= 2026: wraps = sum(_calc(sim, name, year) for name in STATE_WRAPS) - assert (wraps[one_claimed] > 0).any() + assert (wraps[one_claimable] > 0).any() values = {name: _calc(sim, name, year) for name in outputs} values["eligible members"] = _tax_unit_sum(sim, "is_aca_ptc_eligible", year) for name, value in values.items(): @@ -170,45 +225,44 @@ def check_swap(units, year, outputs, require_coverage=False): def check_monotone(units, year): sim = Simulation(situation=_situation(units, year, _monotone_variants)) - had_eligible = (_tax_unit_sum(sim, "is_aca_ptc_eligible", year) > 0).reshape(-1, 4) - enrollees = _tax_unit_sum(sim, "pays_aca_premium", year).reshape(-1, 4) - for name, guard in ( - ("aca_tax_family_size", None), - ("aca_ptc_slcsp", "same enrollees"), - ("aca_ptc", "had eligible"), - ): + fraction = _calc(sim, "aca_magi_fraction", year).reshape(-1, 4) + magi = _calc(sim, "aca_magi", year).reshape(-1, 4) + enrollees = _enrollment_signature(sim, year).reshape(-1, 4) + for name in ("aca_tax_family_size", "aca_ptc_slcsp", "aca_ptc"): values = _calc(sim, name, year).reshape(-1, 4) - nobody, first, second, both = values.T - for more, fewer, base, label in ( - (first, nobody, 0, "first adult"), - (second, nobody, 0, "second adult"), - (both, first, 1, "both after first"), - (both, second, 2, "both after second"), + for more, fewer, label in ( + (1, 0, "first adult"), + (2, 0, "second adult"), + (3, 1, "both after first"), + (3, 2, "both after second"), ): - rises = more > fewer + TOLERANCE - if guard == "had eligible": - rises &= had_eligible[:, base] - elif guard == "same enrollees": - more_index = {"first adult": 1, "second adult": 2}.get(label, 3) - rises &= enrollees[:, base] == enrollees[:, more_index] + rises = values[:, more] > values[:, fewer] + TOLERANCE + if name == "aca_ptc_slcsp": + rises &= enrollees[:, more] == enrollees[:, fewer] + if name == "aca_ptc": + crosses_floor = (fraction[:, fewer] < 1) & (fraction[:, more] >= 1) + income_left = magi[:, more] < magi[:, fewer] - TOLERANCE + rises &= ~crosses_floor & ~income_left assert not rises.any(), ( f"{name} in {year} rises when the {label} is claimed " "elsewhere: " + ", ".join( - f"{units[i]['state']} {fewer[i]:.2f} -> {more[i]:.2f}" + f"{units[i]['state']} {values[i, fewer]:.2f} -> " + f"{values[i, more]:.2f}" for i in np.flatnonzero(rises)[:10] ) ) _identities(sim, year) +@pytest.mark.parametrize("year", YEARS) @settings( - max_examples=4, + max_examples=2, deadline=None, derandomize=True, suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], ) -@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=15)) +@given(st.lists(couples(), min_size=6, max_size=15)) def test_claimable_filer_ptc_is_label_free(year, units): check_swap(units, year, OUTPUTS) diff --git a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml index b3780f6fe83..de6bf23e9d8 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml @@ -28,3 +28,114 @@ selected_marketplace_plan_actuarial_value: 0.7 marketplace_csr_actuarial_value: 0 marketplace_effective_actuarial_value: 0.7 + +- name: 2025 one policy covering a claimable spouse gets no cost-sharing reductions (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 45 CFR 155.305(g)(3): a policy gets only the category all its + # enrollees qualify for; the model prices one policy per tax unit. + person_receives_aca: [false, true] + marketplace_csr_eligible: false + +- name: 2025 one policy covering a claimable spouse gets no cost-sharing reductions (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + person_receives_aca: [true, false] + marketplace_csr_eligible: false + +- name: 2025 one policy whose enrollees are all in the family keeps cost-sharing reductions + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + marketplace_csr_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml index 8f2df037394..ea84ca29d74 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml @@ -236,3 +236,45 @@ state_code: TX output: aca_fpg: 15_650 + +- name: 2025 the poverty line uses the tax year's family size, not a supplied prior-year size + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + tax_unit_size: {2024: 2, 2025: 3} + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # Form 8962 line 1 is the tax year's family; line 4 takes the 2024 + # guideline for it: 15,060 + 2 * 5,380. tax_unit_fpg for 2024 would + # have used the supplied 2024 size of 2 (20,440). + aca_tax_family_size: 3 + aca_fpg: 25_820 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml index 1a5d780bf1d..eb7114d16ee 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml @@ -41,3 +41,215 @@ # Form 8962 Worksheet 1-1 line 3 takes Form 2555 lines 45 and 50. medicaid_magi: 65_000 aca_magi: 65_000 + +- name: 2025 joint return with a claimable spouse leaves the child's tax-exempt interest out of household income (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-1(e)(1): household income counts other individuals only + # if they are in the family; this return's child is not (IRC 152(b)(1)). + adjusted_gross_income: 20_000 + medicaid_magi: 40_000 + aca_magi: 20_000 + +- name: 2025 joint return with a claimable spouse leaves the child's tax-exempt interest out of household income (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + adjusted_gross_income: 20_000 + medicaid_magi: 40_000 + aca_magi: 20_000 + +- name: 2025 joint return with neither spouse claimable keeps household income as medicaid_magi + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + aca_magi: 40_000 + +- name: 2025 refund-only exception keeps the child in the family and their income in household income (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + aca_magi: 40_000 + +- name: 2025 refund-only exception keeps the child in the family and their income in household income (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_magi: 40_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml index 3133369a77d..07a0ecbaf5e 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml @@ -231,3 +231,95 @@ state_code: TX output: aca_tax_family_size: 2 + +- name: "2025 joint return, claimable spouse with the refund-only exception, two children: family of three (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child, child2] + state_code: TX + output: + # Publication 501 (2025) p.11, Dependent Taxpayer Test: the return can claim + # dependents when the would-be claimant files only for a refund. + aca_tax_family_size: 3 + +- name: "2025 joint return, claimable spouse with the refund-only exception, two children: family of three (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child, child2] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child, child2] + state_code: TX + output: + aca_tax_family_size: 3 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml index 7a150cdf659..14c88a14ceb 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -207,3 +207,227 @@ state_code: TX output: is_aca_tax_family_member: [true, true] + +- name: "2025 joint return, claimable spouse whose claimant meets the refund-only exception: spouse out, child in (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # Publication 501 (2025) p.11, Dependent Taxpayer Test: the return can claim + # dependents when the would-be claimant files only for a refund. + # Form 1040 line 12a still marks the spouse as claimable (2025 instructions p.33). + is_aca_tax_family_member: [false, true, true] + +- name: "2025 joint return, claimable spouse whose claimant meets the refund-only exception: spouse out, child in (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + is_aca_tax_family_member: [true, false, true] + +- name: 2025 single claimable filer with the refund-only exception has no applicable taxpayer, so no family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + # IRC 36B(c)(1)(D): no credit to someone another taxpayer can claim. + is_aca_tax_family_member: [false, false] + +- name: 2025 joint return, spouse claimable but not actually claimed is still out of the family (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 26 CFR 1.36B-2(b)(3): 'may claim', whether or not claimed. + is_aca_tax_family_member: [false, true] + +- name: 2025 joint return, spouse claimable but not actually claimed is still out of the family (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + is_aca_tax_family_member: [true, false] + +- name: "2025 tax unit with neither head nor spouse: a claimable 17-year-old filer is out of the family" + period: 2025 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + households: + household: + members: [minor] + state_code: TX + output: + # Role inference gives a lone minor no head; own claimability decides. + is_tax_unit_head: [false] + is_aca_tax_family_member: [false] + +- name: "2025 tax unit with neither head nor spouse: an unclaimed 17-year-old filer stays in the family" + period: 2025 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + households: + household: + members: [minor] + state_code: TX + output: + is_tax_unit_head: [false] + is_aca_tax_family_member: [true] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 462efa51568..8d7f816bc8d 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -726,3 +726,249 @@ state_code: TX output: aca_ptc: 0 + +- name: 2025 child's tax-exempt interest no longer cuts the credit of a return with a claimable spouse (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 20,000 / 15,060 = 132% FPL: 0% contribution; benchmark 6,000 (child out). + aca_magi: 20_000 + aca_magi_fraction: 1.32 + aca_ptc: 6000 + +- name: 2025 child's tax-exempt interest no longer cuts the credit of a return with a claimable spouse (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_magi: 20_000 + aca_magi_fraction: 1.32 + aca_ptc: 6000 + +- name: "2025 refund-only exception: credit for the other spouse and the child (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 30,000 / 20,440 = 146% FPL: 0% contribution; benchmark 6,000 + 3,000. + aca_tax_family_size: 2 + aca_ptc_slcsp: 9000 + aca_ptc: 9000 + +- name: "2025 refund-only exception: credit for the other spouse and the child (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_tax_family_size: 2 + aca_ptc_slcsp: 9000 + aca_ptc: 9000 + +- name: 2025 claimable 17-year-old filing alone gets no credit + period: 2025 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [minor] + state_code: TX + output: + is_aca_ptc_eligible: [false] + aca_ptc: 0 + +- name: 2025 unclaimed 17-year-old filing alone keeps the credit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [minor] + state_code: TX + output: + is_aca_ptc_eligible: [true] + aca_ptc: 6000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml new file mode 100644 index 00000000000..bf7ceae45b0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml @@ -0,0 +1,195 @@ +- name: "2025 joint return with a claimable spouse on the policy: half the premium is the coverage family's (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # Silver plan at 0.75 of the 12,000 benchmark: 9,000. 26 CFR 1.36B-3(h)(1): + # the coverage family's share is 6,000 / 12,000, so 4,500. + selected_marketplace_plan_premium_proxy: 9000 + aca_ptc_coverage_family_premium: 4500 + +- name: "2025 joint return with a claimable spouse on the policy: half the premium is the coverage family's (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + selected_marketplace_plan_premium_proxy: 9000 + aca_ptc_coverage_family_premium: 4500 + +- name: "2025 joint return with neither spouse claimable: the whole premium is the coverage family's" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_coverage_family_premium: 9000 + +- name: 2025 bronze plan is allocated by the same benchmark share (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_category: BRONZE + lcbp: 7000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 7,000 * 6,000 / 12,000. + aca_ptc_coverage_family_premium: 3500 + +- name: 2025 bronze plan is allocated by the same benchmark share (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_category: BRONZE + lcbp: 7000 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc_coverage_family_premium: 3500 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml index ddf141f5162..c5a2aa7a740 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml @@ -55,3 +55,80 @@ selected_marketplace_plan_premium_proxy: 5_000 used_aca_ptc: 3_000 marketplace_net_premium: 2_000 + +- name: 2025 the claimable spouse's share of the premium stays unsubsidized (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 9,000 - 4,500. + marketplace_net_premium: 4500 + +- name: 2025 the claimable spouse's share of the premium stays unsubsidized (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + marketplace_net_premium: 4500 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml index c1f36cf129c..776d01b85dd 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml @@ -74,3 +74,82 @@ selected_marketplace_plan_premium_proxy: 7_000 used_aca_ptc: 7_000 unused_aca_ptc: 2_200 + +- name: 2025 credit is capped at the coverage family's share of the premium (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # min(6,000, 4,500); before, the cap was the whole 9,000 premium. + aca_ptc: 6000 + used_aca_ptc: 4500 + +- name: 2025 credit is capped at the coverage family's share of the premium (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc: 6000 + used_aca_ptc: 4500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml index 0df47cd45e4..0b311e29096 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml @@ -843,3 +843,86 @@ output: # Neither spouse is in a federal tax family. co_premium_assistance: 0 + +- name: 2026 CO joint return with one claimable spouse where the residual premium binds (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 20_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # 20,000 / 15,650 = 127% FPL, 2.1%: contribution 420, aca_ptc 5,580. + # min(12 * 80, 6,000 - 5,580) = 420; the whole-enrollment benchmark + # would give min(960, 12,000 - 5,580) = 960. + aca_ptc: 5580 + co_premium_assistance: 420 + +- name: 2026 CO joint return with one claimable spouse where the residual premium binds (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 20_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CO + output: + aca_ptc: 5580 + co_premium_assistance: 420 From a4d97502eb88caebdbfe7a33f4039885b05ebf80 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 08:12:09 -0400 Subject: [PATCH 22/32] Address review r2: role-less filers, coverage-family pricing and allocation, MAGI floor - A tax unit with neither a head nor a spouse is filed by its oldest member. When that member can be claimed elsewhere, nobody is in the tax family. Before, a claimable 17-year-old filing with an infant got $12,000. - The coverage family is priced on its own. Its children are ranked for the three-oldest-children rule (45 CFR 147.102(c)(1)), and its family tier is built from its own members. - A shared premium is allocated by each family's benchmark (26 CFR 1.36B-3(h)(1)). The denominator is the coverage family's benchmark plus the benchmark of the enrollees outside it, not the whole policy's tier. An empty coverage family gets no share of the premium. - Out-of-family dependents' tax-exempt interest and Social Security are removed before the single zero floor. A negative benefit no longer cuts household income. - is_aca_marketplace_enrollee (enrolled) is split from pays_aca_premium (charged an age-rated premium). The family-tier child tests no longer depend on payment. - Tests: YAML for each case under both head/spouse labellings. The property test now gives dependents tax-exempt interest and Social Security. It checks aca_magi against an expectation built from the inputs, and it exempts only that removal from monotonicity. Monotonicity runs under Hypothesis in every year 2021-2026. The "unchanged" identities apply only to returns with a head or spouse. Co-Authored-By: Claude Opus 5.5 --- .../test_claimable_filer_ptc_invariants.py | 190 +++++++++++++++--- .../gov/aca/eligibility/aca_magi.yaml | 85 ++++++++ .../is_aca_marketplace_enrollee.yaml | 64 ++++++ .../eligibility/is_aca_tax_family_member.yaml | 66 ++++++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 71 +++++++ .../ptc/aca_ptc_coverage_family_premium.yaml | 144 +++++++++++++ .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 132 ++++++++++++ .../variables/gov/aca/eligibility/aca_magi.py | 29 ++- .../is_aca_coverage_family_member.py | 14 +- .../is_aca_marketplace_enrollee.py | 56 ++++++ .../eligibility/is_aca_tax_family_member.py | 17 +- .../gov/aca/eligibility/pays_aca_premium.py | 47 +---- .../ptc/aca_ptc_coverage_family_premium.py | 37 ++-- .../variables/gov/aca/slspc/aca_ptc_slcsp.py | 69 +++++-- .../is_aca_family_tier_dependent_child.py | 29 ++- .../slspc/is_aca_ny_age_29_dependent_child.py | 25 ++- .../aca/slspc/slcsp_family_tier_category.py | 5 +- .../aca/slspc/slcsp_family_tier_multiplier.py | 10 +- 18 files changed, 953 insertions(+), 137 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py index afde18c3e02..134745ca608 100644 --- a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -11,27 +11,36 @@ it follow. For couples drawn by Hypothesis in every year 2021-2026 (spouses claimable, -actually claimed or neither, with or without the filing exception) and a -seeded population in 2025 and 2026: +actually claimed or neither, with or without the filing exception, and +dependents with tax-exempt interest and Social Security) and a seeded +population in 2025 and 2026 that includes review r2's family and premium +cases: 1. Swap invariance: each output is the same under either head/spouse labelling. 2. Identities: `aca_tax_family_size` equals the filers who cannot be claimed plus the dependents when one of them is an applicable taxpayer and no filer is claimable outside the exception; nobody outside the tax family is - eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; `aca_magi` <= `medicaid_magi`; - used credit <= coverage-family premium <= selected premium; cost-sharing - reductions only when every enrollee is eligible for the credit; no family - member means no credit; and where no filer can be claimed, `aca_ptc_slcsp` - = `slcsp`, `aca_magi` = `medicaid_magi`, `aca_tax_family_size` = - `tax_unit_size` and `aca_fpg` = `tax_unit_fpg` for the prior year (these - simulations supply no prior-year size, so the two sizes agree). -3. Monotonicity: marking another filer as claimed never raises the tax family - size, never raises the benchmark while the same people are enrolled, and - never raises the credit, except in two intended cases: income crosses the - 100% floor (below it for two, above it for one, so IRC 36B allows a credit - that was not allowed), or out-of-family dependents' income leaves household - income. + eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; `aca_magi` is AGI plus the + MAGI additions, less the tax-exempt interest and Social Security of + dependents outside the family, floored at zero once (computed here from + the inputs, not from the model's family variables); used credit <= + coverage-family premium <= selected premium; no family member means no + credit and no coverage-family premium; cost-sharing reductions only when + every enrollee is eligible for the credit; and on a return with a head or + spouse where neither can be claimed, `aca_ptc_slcsp` = `slcsp`, + `aca_magi` = `medicaid_magi`, `aca_tax_family_size` = `tax_unit_size` and + `aca_fpg` = `tax_unit_fpg` for the prior year (these simulations supply no + prior-year size, so the two sizes agree). A unit with neither a head nor a + spouse is filed by its oldest member, so a claimable minor changes it. +3. Monotonicity, for the drawn couples in every year and the seeded + population: marking another filer as claimable never raises the tax + family size, never raises the benchmark while the same people are + enrolled, and never raises the credit, except in two intended cases: + income crosses the 100% floor (below it for two, above it for one, so IRC + 36B allows a credit that was not allowed), or dependents leave the family + and their tax-exempt interest and Social Security leave household income + (identity 2 checks the amount removed). """ import numpy as np @@ -51,6 +60,7 @@ _swap_variants, adults, dependents, + money, ) # Texas (age curve, no expansion), New York and Vermont (family tiers), and @@ -81,6 +91,18 @@ "wa_cascade_care_savings", ] OUTPUTS = FEDERAL + STATE_WRAPS +INCOME_SEED = 20261012 + + +@st.composite +def ptc_dependents(draw): + # Tax-exempt interest and Social Security count in household income only + # while the dependent is in the family. + return { + **draw(dependents()), + "tax_exempt_interest_income": draw(money(20_000)), + "social_security_survivors": draw(money(12_000)), + } @st.composite @@ -91,7 +113,7 @@ def couples(draw): return { "state": draw(st.sampled_from(STATES)), "adults": [draw(adults()), draw(adults())], - "dependents": draw(st.lists(dependents(), max_size=2)), + "dependents": draw(st.lists(ptc_dependents(), max_size=2)), "claimed": claimed, "claimable": (claimed[0] or extra[0], claimed[1] or extra[1]), "exception": draw(st.sampled_from(CLAIM_PATTERNS)), @@ -122,6 +144,67 @@ def _ptc_cases(states): return cases +def _review_r2_cases(): + # A New York family-tier policy and a Texas coverage family of one adult + # and three children, each beside a claimable spouse whose claimant meets + # the refund-only exception, and dependents whose tax-exempt interest or + # negative Social Security leaves household income. + adult = {"age": 30, "employment_income": 11_000.0} + exception = {"claimable": (True, False), "exception": (True, False)} + return [ + { + "state": "NY", + "adults": [adult, adult], + "dependents": [{"age": 10}], + "claimed": (False, False), + **exception, + }, + { + "state": "TX", + "adults": [ + {"age": 19, "employment_income": 11_000.0}, + {"age": 30, "employment_income": 29_000.0}, + ], + "dependents": [{"age": 10}, {"age": 8}, {"age": 6}], + "claimed": (False, False), + **exception, + }, + { + "state": "TX", + "adults": [adult, adult], + "dependents": [{"age": 10, "tax_exempt_interest_income": 20_000.0}], + "claimed": (True, False), + }, + { + "state": "TX", + "adults": [adult, adult], + "dependents": [{"age": 10, "social_security_survivors": -30_000.0}], + "claimed": (True, False), + }, + ] + + +def _with_dependent_income(units): + # Some seeded dependents get tax-exempt interest or Social Security. + rng = np.random.default_rng(INCOME_SEED) + out = [] + for unit in units: + dependents_with_income = [] + for amounts in unit["dependents"]: + amounts = dict(amounts) + if rng.random() < 0.5: + amounts["tax_exempt_interest_income"] = float( + round(rng.uniform(1, 20_000)) + ) + if rng.random() < 0.3: + amounts["social_security_survivors"] = float( + round(rng.uniform(1, 12_000)) + ) + dependents_with_income.append(amounts) + out.append({**unit, "dependents": dependents_with_income}) + return out + + def _tax_unit_sum(sim, name, year): return np.asarray(sim.calculate(name, year, map_to="tax_unit"), dtype=float) @@ -143,6 +226,40 @@ def _enrollment_signature(sim, year): return _tax_unit_sum_array(sim, enrolled * 2.0**position) +def _expected_aca_magi(sim, year): + # 26 CFR 1.36B-1(e)(1): household income counts a dependent's income only + # if the return claims them. A return claims none when no filer is an + # applicable taxpayer or when a filer can be claimed and the claimant + # does not meet the refund-only exception. Built from the inputs. + filer = np.asarray(sim.calculate("is_tax_unit_head_or_spouse", year)) + claimable = np.asarray( + sim.calculate("claimable_as_dependent_on_another_return", year) + ) + exception = np.asarray(sim.calculate("dependent_claimant_filing_exception", year)) + independent = _tax_unit_sum_array(sim, filer & ~claimable) + blocked = _tax_unit_sum_array(sim, filer & claimable & ~exception) > 0 + dependent_income = _tax_unit_sum_array( + sim, + ~filer + * ( + _calc(sim, "tax_exempt_interest_income", year) + + _calc(sim, "social_security", year) + ), + ) + removed = np.where((independent == 0) | blocked, dependent_income, 0) + unfloored = _calc(sim, "adjusted_gross_income", year) + additions = sim.tax_benefit_system.parameters( + f"{year}-01-01" + ).gov.hhs.medicaid.income.modification + for name in additions: + if sim.tax_benefit_system.variables[name].entity.key == "person": + unfloored = unfloored + _tax_unit_sum(sim, name, year) + else: + unfloored = unfloored + _calc(sim, name, year) + roleful = _tax_unit_sum_array(sim, filer) > 0 + return np.maximum(0, unfloored - removed), removed, roleful + + def _identities(sim, year): claimable_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 blocked = ( @@ -167,7 +284,10 @@ def _identities(sim, year): assert (benchmark <= slcsp + TOLERANCE).all(), "benchmark above slcsp" aca_magi = _calc(sim, "aca_magi", year) medicaid_magi = _calc(sim, "medicaid_magi", year) - assert (aca_magi <= medicaid_magi + TOLERANCE).all() + expected_magi, _, roleful = _expected_aca_magi(sim, year) + np.testing.assert_allclose( + aca_magi[roleful], expected_magi[roleful], atol=TOLERANCE + ) premium = _calc(sim, "selected_marketplace_plan_premium_proxy", year) family_premium = _calc(sim, "aca_ptc_coverage_family_premium", year) used = _calc(sim, "used_aca_ptc", year) @@ -178,7 +298,8 @@ def _identities(sim, year): ineligible_enrollees = _tax_unit_sum_array(sim, enrolled & ~eligible) assert not (csr & (ineligible_enrollees > 0)).any() assert not _calc(sim, "aca_ptc", year)[size == 0].any() - unclaimed = ~claimable_filer + assert not family_premium[size == 0].any() + unclaimed = roleful & ~claimable_filer np.testing.assert_allclose(benchmark[unclaimed], slcsp[unclaimed], atol=TOLERANCE) np.testing.assert_allclose( aca_magi[unclaimed], medicaid_magi[unclaimed], atol=TOLERANCE @@ -223,10 +344,18 @@ def check_swap(units, year, outputs, require_coverage=False): _identities(sim, year) -def check_monotone(units, year): - sim = Simulation(situation=_situation(units, year, _monotone_variants)) +def check_monotone(units, year, require_removal=False): + # Each filer is claimable exactly when claimed in the variant; drawn + # filing exceptions stay. + sim = Simulation( + situation=_situation( + units, year, _monotone_variants, explicit_claimability=True + ) + ) fraction = _calc(sim, "aca_magi_fraction", year).reshape(-1, 4) - magi = _calc(sim, "aca_magi", year).reshape(-1, 4) + removed = _expected_aca_magi(sim, year)[1].reshape(-1, 4) + if require_removal: + assert (removed > 0).any(), "no dependent income leaves household income" enrollees = _enrollment_signature(sim, year).reshape(-1, 4) for name in ("aca_tax_family_size", "aca_ptc_slcsp", "aca_ptc"): values = _calc(sim, name, year).reshape(-1, 4) @@ -241,7 +370,7 @@ def check_monotone(units, year): rises &= enrollees[:, more] == enrollees[:, fewer] if name == "aca_ptc": crosses_floor = (fraction[:, fewer] < 1) & (fraction[:, more] >= 1) - income_left = magi[:, more] < magi[:, fewer] - TOLERANCE + income_left = removed[:, more] > removed[:, fewer] + TOLERANCE rises &= ~crosses_floor & ~income_left assert not rises.any(), ( f"{name} in {year} rises when the {label} is claimed " @@ -267,12 +396,25 @@ def test_claimable_filer_ptc_is_label_free(year, units): check_swap(units, year, OUTPUTS) +@pytest.mark.parametrize("year", YEARS) +@settings( + max_examples=2, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(couples(), min_size=6, max_size=15)) +def test_claimable_filer_ptc_is_monotone(year, units): + check_monotone(units, year) + + @pytest.mark.parametrize("year", [2025, 2026]) def test_claimable_filer_ptc_seeded_population(year): units = ( - _seeded_couples(STATES, per_state=2) + _with_dependent_income(_seeded_couples(STATES, per_state=2)) + _crafted_cases(STATES, review_case_only=True) + _ptc_cases(STATES) + + _review_r2_cases() ) check_swap(units, year, OUTPUTS, require_coverage=True) - check_monotone(units, year) + check_monotone(units, year, require_removal=True) diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml index eb7114d16ee..d2bb20d5090 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml @@ -253,3 +253,88 @@ state_code: TX output: aca_magi: 40_000 + +- name: 2025 an excluded dependent's negative Social Security is removed before the zero floor (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + social_security: -30_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # max(0, 20,000 - 30,000 - (-30,000)) = 20,000. + medicaid_magi: 0 + aca_magi: 20_000 + +- name: 2025 an excluded dependent's negative Social Security is removed before the zero floor (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + social_security: -30_000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + medicaid_magi: 0 + aca_magi: 20_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml new file mode 100644 index 00000000000..b8dee264d14 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml @@ -0,0 +1,64 @@ +- name: 2025 a fourth child is enrolled but pays no age-rated premium + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child2: + age: 8 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child3: + age: 6 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child4: + age: 4 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2, child3, child4] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child, child2, child3, child4] + state_code: TX + output: + # 45 CFR 147.102(c)(1): premiums for the three oldest children only. + is_aca_marketplace_enrollee: [true, true, true, true, true, true] + pays_aca_premium: [true, true, true, true, true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml index 14c88a14ceb..4e7fbfc067e 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -431,3 +431,69 @@ output: is_tax_unit_head: [false] is_aca_tax_family_member: [true] + +- name: "2025 claimable 17-year-old filing with an infant: neither is in a family" + period: 2025 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + households: + household: + members: [parent, infant] + state_code: TX + output: + # Role inference makes the oldest adult the head, so this unit has none; the + # oldest member is taken as the filer (IRC 36B(c)(1)(D)). + is_tax_unit_head: [false, false] + is_aca_tax_family_member: [false, false] + +- name: "2025 unclaimed 17-year-old filing with an infant: both are in the family" + period: 2025 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + households: + household: + members: [parent, infant] + state_code: TX + output: + is_aca_tax_family_member: [true, true] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 8d7f816bc8d..01b99f555c2 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -972,3 +972,74 @@ output: is_aca_ptc_eligible: [true] aca_ptc: 6000 + +- name: 2025 claimable 17-year-old filing with an infant gets no credit + period: 2025 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [parent, infant] + state_code: TX + output: + # Role inference makes the oldest adult the head, so this unit has none; the + # oldest member is taken as the filer (IRC 36B(c)(1)(D)). + aca_ptc: 0 + +- name: 2025 unclaimed 17-year-old filing with an infant gets the credit for both + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [parent, infant] + state_code: TX + output: + # 22,590 / 20,440 = 110% FPL: 0% contribution; 2 * 6,000. + aca_ptc: 12_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml index bf7ceae45b0..7d80d1157d5 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml @@ -193,3 +193,147 @@ state_code: TX output: aca_ptc_coverage_family_premium: 3500 + +- name: "2025 NY policy with a claimable spouse (refund-only exception) and a child: allocate by each family's tier (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.5 + households: + household: + members: [person_a, person_b, child] + state_code: NY + slcsp_age_0: 6000 + slcsp_rating_area: 3 + output: + # NY tiers on 6,000: whole policy 2.85 = 17,100; coverage family (one + # adult and a child) 1.70 = 10,200; the claimable spouse alone 1.00 = + # 6,000. 26 CFR 1.36B-3(h)(1): 8,550 * 10,200 / (10,200 + 6,000). + slcsp: 17_100 + aca_ptc_slcsp: 10_200 + selected_marketplace_plan_premium_proxy: 8550 + aca_ptc_coverage_family_premium: 5383.33 + +- name: "2025 NY policy with a claimable spouse (refund-only exception) and a child: allocate by each family's tier (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.5 + households: + household: + members: [person_b, person_a, child] + state_code: NY + slcsp_age_0: 6000 + slcsp_rating_area: 3 + output: + slcsp: 17_100 + aca_ptc_slcsp: 10_200 + selected_marketplace_plan_premium_proxy: 8550 + aca_ptc_coverage_family_premium: 5383.33 + +- name: "2025 both spouses claimable: no coverage family, so none of a bronze premium" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_category: BRONZE + lcbp: 7000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_coverage_family_premium: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml index 51ae163bac9..5a70e1a382f 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -370,3 +370,135 @@ output: slcsp: 2135.6 aca_ptc_slcsp: 760 + +- name: 2025 coverage family's three oldest children are charged once a claimable 19-year-old spouse is out (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 19 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child2: + age: 8 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child3: + age: 6 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2, child3] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child, child2, child3] + state_code: TX + slcsp_age_0: 1000 + slcsp_rating_area: 1 + output: + # 45 CFR 147.102(c)(1): the three oldest covered children. The whole + # tax unit ranks the 19-year-old first, so the youngest child pays no + # premium in slcsp; the coverage family (the other spouse and all + # three children) charges all three. Default age curve on 1,000. + pays_aca_premium: [true, true, true, true, false] + is_aca_marketplace_enrollee: [true, true, true, true, true] + slcsp: 4713.8 + aca_ptc_slcsp: 4483.7 + +- name: 2025 coverage family's three oldest children are charged once a claimable 19-year-old spouse is out (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 19 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child2: + age: 8 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child3: + age: 6 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child, child2, child3] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a, child, child2, child3] + state_code: TX + slcsp_age_0: 1000 + slcsp_rating_area: 1 + output: + pays_aca_premium: [true, true, true, true, false] + is_aca_marketplace_enrollee: [true, true, true, true, true] + slcsp: 4713.8 + aca_ptc_slcsp: 4483.7 diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py index bdd10aa10ac..118e4f16533 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py @@ -15,20 +15,23 @@ class aca_magi(Variable): "Household income for the premium tax credit (IRC 36B(d)(2), 26 CFR " "1.36B-1(e)(1)): the taxpayer's modified AGI, both spouses' on a " "joint return, plus that of other members of the tax family. It is " - "medicaid_magi without the tax-exempt interest and Social Security " - "of the return's dependents who are outside the tax family " - "(is_aca_tax_family_member), which happens only on a return whose " - "head or spouse can be claimed elsewhere. medicaid_magi adds those " - "two amounts for every member (tax unit AGI already leaves " - "dependents' income out); both are nonnegative, so removing them " - "after medicaid_magi's zero floor gives the same result as removing " - "them before it." + "medicaid_magi's AGI plus MAGI additions, without the tax-exempt " + "interest and Social Security of the return's dependents who are " + "outside the tax family (is_aca_tax_family_member), floored at zero " + "once. Those amounts are left out only on a return whose head or " + "spouse can be claimed elsewhere (or, in a tax unit with neither, " + "whose oldest member can be); otherwise this equals medicaid_magi. " + "Tax unit AGI already leaves dependents' income out." ) def formula(tax_unit, period, parameters): person = tax_unit.members filer = person("is_tax_unit_head_or_spouse", period) - outside_family = ~filer & ~person("is_aca_tax_family_member", period) + has_filer = person.tax_unit.any(filer) + age = person("age", period) + oldest = person.get_rank(tax_unit, -age) == 0 + files_return = where(has_filer, filer, oldest) + outside_family = ~files_return & ~person("is_aca_tax_family_member", period) excluded = tax_unit.sum( outside_family * ( @@ -36,4 +39,10 @@ def formula(tax_unit, period, parameters): + person("social_security", period) ) ) - return max_(0, tax_unit("medicaid_magi", period) - excluded) + # medicaid_magi's AGI plus additions, before its zero floor, so a + # negative excluded amount is removed before the floor too. + additions = parameters(period).gov.hhs.medicaid.income.modification + unfloored = tax_unit("adjusted_gross_income", period) + add( + tax_unit, period, additions + ) + return max_(0, unfloored - excluded) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py index 389b06a6a8c..68907d3f4e4 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py @@ -8,15 +8,15 @@ class is_aca_coverage_family_member(Variable): definition_period = YEAR reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2" documentation = ( - "A Marketplace enrollee (pays_aca_premium) who is in the premium tax " - "credit tax family. 26 CFR 1.36B-3(b)(2) defines the coverage family " - "as the members of the taxpayer's family for whom the month is a " - "coverage month, and the benchmark plan in 1.36B-3(f) is the one " - "offered to the coverage family. An enrollee who can be claimed on " - "another return buys a plan but is not in it." + "A Marketplace enrollee (is_aca_marketplace_enrollee) who is in the " + "premium tax credit tax family. 26 CFR 1.36B-3(b)(2) defines the " + "coverage family as the members of the taxpayer's family for whom the " + "month is a coverage month, and the benchmark plan in 1.36B-3(f) is " + "the one offered to the coverage family. An enrollee who can be " + "claimed on another return buys a plan but is not in it." ) def formula(person, period, parameters): - return person("pays_aca_premium", period) & person( + return person("is_aca_marketplace_enrollee", period) & person( "is_aca_tax_family_member", period ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py new file mode 100644 index 00000000000..022a6059d4d --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py @@ -0,0 +1,56 @@ +from policyengine_us.model_api import * + + +class is_aca_marketplace_enrollee(Variable): + value_type = bool + entity = Person + label = "Person is enrolled in Marketplace coverage" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/cfr/text/45/155.305#a", + "https://www.law.cornell.edu/uscode/text/26/36B#c_1", + ) + documentation = ( + "Whether a person is enrolled in a Marketplace plan: the TIN, " + "immigration-status, other-minimum-essential-coverage and Medicaid " + "coverage-gap conditions of pays_aca_premium, without its premium " + "condition. A child beyond the three oldest is enrolled but pays no " + "age-rated premium (45 CFR 147.102(c)(1)); pays_aca_premium is this " + "variable restricted to those who pay one." + ) + + def formula(person, period, parameters): + immigration_eligible = person("is_aca_ptc_immigration_status_eligible", period) + taxpayer_has_tin = person.tax_unit("taxpayer_has_tin", period) + is_status_eligible = taxpayer_has_tin & immigration_eligible + + p = parameters(period).gov.aca + is_coverage_eligible = add(person, period, p.ineligible_coverage) == 0 + + # The Medicaid coverage gap: under the premium tax credit's income + # floor, income-ineligible under the eligibility scale itself, with no + # Medicaid pathway (already required by ineligible_coverage above: a + # non-expansion state, or a status bar), and without the below-poverty + # exception for lawfully present immigrants, which 26 U.S.C. + # 36B(c)(1)(B) carried until P.L. 119-21 repealed it from 2026. The + # scale's own result is read, not just its threshold, so a reform that + # makes the first bracket income-eligible reopens the credit here + # instead of being blocked by a threshold that did not move. The model + # assumes nobody in that gap buys the full-price benchmark plan; + # charging it made a Wyoming adult at $0 of income pay $11,769 a year + # (policyengine-us #9472). Only the floor is gated: above the + # eligibility ceiling, where the 2026 subsidy cliff returns, full + # price is what an enrollee pays. + income_eligibility = p.ptc_income_eligibility + magi_frac = person.tax_unit("aca_magi_fraction", period) + floor = income_eligibility.thresholds[1] + below_fpl_exception = person.tax_unit( + "aca_ptc_below_fpl_immigration_exception", period + ) + in_coverage_gap = ( + (magi_frac < floor) + & ~income_eligibility.calc(magi_frac) + & ~below_fpl_exception + ) + + return is_status_eligible & is_coverage_eligible & ~in_coverage_gap diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py index cac4db03b6c..48294317543 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -28,8 +28,11 @@ class is_aca_tax_family_member(Variable): "Dependent Taxpayer Test). No credit is allowed to someone another " "taxpayer can claim (IRC 36B(c)(1)(D)), so a return whose every head " "and spouse can be claimed has no applicable taxpayer and no family " - "for the credit. A tax unit with neither a head nor a spouse has no " - "modeled filer, so each member's own claimability decides. A " + "for the credit. A tax unit with neither a head nor a spouse (role " + "inference makes the oldest adult the head, so a unit of minors has " + "none) is treated as filed by its oldest member: everyone is in the " + "family if that member cannot be claimed elsewhere, and nobody is if " + "they can. A " "dependent on a return with an applicable taxpayer is a member even if " "another taxpayer could also claim them: this return claims them." ) @@ -45,4 +48,12 @@ def formula(person, period, parameters): "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) dependent_member = applicable_taxpayer & claims_dependents - return where(has_filer, where(filer, ~claimable, dependent_member), ~claimable) + # No head or spouse: the oldest member files. + age = person("age", period) + oldest = person.get_rank(person.tax_unit, -age) == 0 + oldest_claimable = person.tax_unit.any(oldest & claimable) + return where( + has_filer, + where(filer, ~claimable, dependent_member), + ~oldest_claimable, + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py b/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py index 75c148f4766..044d4f01591 100644 --- a/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py +++ b/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py @@ -24,49 +24,16 @@ class pays_aca_premium(Variable): "does, because the unsubsidized benchmark premium exceeds their " "income (policyengine-us #9472). A reader that wants the status and " "coverage conditions without that assumption should not reuse this " - "variable." + "variable. It is is_aca_marketplace_enrollee restricted to people " + "charged an age-rated premium: adults, and the three oldest children " + "in the tax unit." ) def formula(person, period, parameters): - immigration_eligible = person("is_aca_ptc_immigration_status_eligible", period) - taxpayer_has_tin = person.tax_unit("taxpayer_has_tin", period) - is_status_eligible = taxpayer_has_tin & immigration_eligible - + # An enrollee pays an age-rated premium unless they are a child + # beyond the three oldest (45 CFR 147.102(c)(1)). + enrollee = person("is_aca_marketplace_enrollee", period) p = parameters(period).gov.aca - is_coverage_eligible = add(person, period, p.ineligible_coverage) == 0 is_aca_adult = person("age", period) > p.slcsp.max_child_age child_pays = person("aca_child_index", period) <= p.max_child_count - pays_age_based_premium = is_aca_adult | child_pays - - # The Medicaid coverage gap: under the premium tax credit's income - # floor, income-ineligible under the eligibility scale itself, with no - # Medicaid pathway (already required by ineligible_coverage above: a - # non-expansion state, or a status bar), and without the below-poverty - # exception for lawfully present immigrants, which 26 U.S.C. - # 36B(c)(1)(B) carried until P.L. 119-21 repealed it from 2026. The - # scale's own result is read, not just its threshold, so a reform that - # makes the first bracket income-eligible reopens the credit here - # instead of being blocked by a threshold that did not move. The model - # assumes nobody in that gap buys the full-price benchmark plan; - # charging it made a Wyoming adult at $0 of income pay $11,769 a year - # (policyengine-us #9472). Only the floor is gated: above the - # eligibility ceiling, where the 2026 subsidy cliff returns, full - # price is what an enrollee pays. - income_eligibility = p.ptc_income_eligibility - magi_frac = person.tax_unit("aca_magi_fraction", period) - floor = income_eligibility.thresholds[1] - below_fpl_exception = person.tax_unit( - "aca_ptc_below_fpl_immigration_exception", period - ) - in_coverage_gap = ( - (magi_frac < floor) - & ~income_eligibility.calc(magi_frac) - & ~below_fpl_exception - ) - - return ( - is_status_eligible - & is_coverage_eligible - & pays_age_based_premium - & ~in_coverage_gap - ) + return enrollee & (is_aca_adult | child_pays) diff --git a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py index d06c3482cf2..d9c00f4f054 100644 --- a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.aca_ptc_slcsp import ( + benchmark_premium, +) class aca_ptc_coverage_family_premium(Variable): @@ -10,24 +13,32 @@ class aca_ptc_coverage_family_premium(Variable): reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#d_1" documentation = ( "The enrollment premiums for the premium tax credit's lesser-of test " - "(26 CFR 1.36B-3(d)(1)(i)): the selected plan premium for the tax " - "unit, of which the coverage family's share is its benchmark " - "(aca_ptc_slcsp) over the whole enrollment's benchmark (slcsp). 26 " - "CFR 1.36B-3(h)(1) allocates a policy covering more than one family " - "'in proportion to the premiums for each taxpayer's applicable " - "benchmark plan'; the whole-enrollment benchmark stands in for the " - "sum of those benchmarks. The share is 1 whenever every enrollee is " - "in the coverage family. It applies to bronze plans too." + "(26 CFR 1.36B-3(d)(1)(i)). The model prices one policy per tax unit. " + "When it also covers enrollees outside the tax family, 26 CFR " + "1.36B-3(h)(1) allocates the premium 'in proportion to the premiums " + "for each taxpayer's applicable benchmark plan': the coverage family's " + "benchmark (aca_ptc_slcsp) over that plus the benchmark of the other " + "enrollees, priced as one family of their own. The whole premium is " + "the coverage family's when every enrollee is in it, and none is when " + "no enrollee is. It applies to bronze plans too." ) def formula(tax_unit, period, parameters): premium = tax_unit("selected_marketplace_plan_premium_proxy", period) - benchmark = add(tax_unit, period, ["slcsp"]) + person = tax_unit.members + enrolled = person("is_aca_marketplace_enrollee", period) + member = person("is_aca_tax_family_member", period) + every_enrollee_covered = ~tax_unit.any(enrolled & ~member) coverage_family_benchmark = add(tax_unit, period, ["aca_ptc_slcsp"]) + other_benchmark = sum( + benchmark_premium(tax_unit, month, parameters, ~member, enrolled) + for month in period.get_subperiods(MONTH) + ) + total = coverage_family_benchmark + other_benchmark share = np.divide( coverage_family_benchmark, - benchmark, - out=np.ones_like(benchmark), - where=benchmark > 0, + total, + out=np.zeros_like(total), + where=total > 0, ) - return premium * share + return premium * where(every_enrollee_covered, 1, share) diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py index ea2eaf3ae9a..a49a4df22a1 100644 --- a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py @@ -1,4 +1,42 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( + family_tier_category, +) +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_multiplier import ( + family_tier_multiplier, +) + + +def benchmark_premium(tax_unit, period, parameters, group, enrolled): + """Monthly second-lowest-cost silver premium for the enrollees of one + family on the return. + + `group` marks the family's members and `enrolled` the Marketplace + enrollees (person-level boolean arrays). Children are ranked within the + family, so only its three oldest children add an age-rated premium (45 CFR + 147.102(c)(1)); in family-tier states the tier is the family's own. + """ + person = tax_unit.members + p = parameters(period).gov.aca + age = person("age", period.this_year) + is_child = age <= p.slcsp.max_child_age + child_rank = person.get_rank(tax_unit, -age, condition=group & is_child) + charged = group & enrolled & (~is_child | (child_rank < p.max_child_count)) + base = person.household("slcsp_age_0", period) + multiplier = person("slcsp_age_curve_multiplier", period) + age_curve_applies = tax_unit("slcsp_age_curve_applies", period) + age_curve = tax_unit.sum(charged * base * multiplier) * age_curve_applies + tier_enrolled = group & enrolled + category = family_tier_category(tax_unit, period, parameters, tier_enrolled) + tier_multiplier = family_tier_multiplier( + tax_unit, period, parameters, category, tier_enrolled + ) + family_tier = where( + tax_unit("slcsp_family_tier_applies", period), + tax_unit.household("slcsp_age_0", period) * tier_multiplier, + 0, + ) + return age_curve + family_tier class aca_ptc_slcsp(Variable): @@ -13,28 +51,25 @@ class aca_ptc_slcsp(Variable): ) documentation = ( "The second-lowest-cost silver plan premium offered to the coverage " - "family (26 CFR 1.36B-3(f)): the enrollees who are in the premium tax " - "credit tax family. When every enrollee on the return is in it, this " - "is slcsp itself, read directly so a supplied slcsp carries through. " - "When an enrollee is not (one who can be claimed on another return, " - "or a dependent of a return with such a filer), it is the age-curve " - "premiums of the coverage family members or, where family tiers " - "apply, the tier premium for them. slcsp keeps every enrollee, since " - "everyone still buys a plan." + "family (26 CFR 1.36B-3(f)): the Marketplace enrollees who are in the " + "premium tax credit tax family. When every enrollee on the return is " + "in it, this is slcsp itself, read directly so a supplied slcsp " + "carries through. When an enrollee is not (one who can be claimed on " + "another return, or a dependent of a return with such a filer), the " + "coverage family is priced on its own: age-curve premiums with its " + "children ranked among themselves, so its three oldest children are " + "charged, or, where family tiers apply, its own tier. slcsp keeps " + "every enrollee, since everyone still buys a plan." ) def formula(tax_unit, period, parameters): person = tax_unit.members - enrolled = person("pays_aca_premium", period.this_year) - covered = person("is_aca_coverage_family_member", period.this_year) - every_enrollee_covered = ~tax_unit.any(enrolled & ~covered) - age_curve = tax_unit.sum( - covered * person("slcsp_age_curve_amount_person", period) - ) - family_tier = tax_unit.household("slcsp_age_0", period) * tax_unit( - "aca_ptc_slcsp_family_tier_multiplier", period + enrolled = person("is_aca_marketplace_enrollee", period.this_year) + member = person("is_aca_tax_family_member", period.this_year) + every_enrollee_covered = ~tax_unit.any(enrolled & ~member) + coverage_family_benchmark = benchmark_premium( + tax_unit, period, parameters, member, enrolled ) - coverage_family_benchmark = age_curve + family_tier return where( every_enrollee_covered, tax_unit("slcsp", period), diff --git a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py index 89231d0249b..82b44cf6cee 100644 --- a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py +++ b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py @@ -1,4 +1,21 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.is_aca_ny_age_29_dependent_child import ( + ny_age_29_child_condition, +) + + +def family_tier_child_condition(person, period, parameters): + """The family-tier dependent-child test without the premium condition.""" + p = parameters(period).gov.aca + age = person("monthly_age", period) + is_tax_dependent = person("is_tax_unit_dependent", period) + under_child_only_age = age <= p.slcsp.max_child_age + under_age_threshold = age < p.family_tier_dependent_child_age_threshold + return ( + under_child_only_age + | (is_tax_dependent & under_age_threshold) + | ny_age_29_child_condition(person, period, parameters) + ) class is_aca_family_tier_dependent_child(Variable): @@ -8,14 +25,6 @@ class is_aca_family_tier_dependent_child(Variable): definition_period = MONTH def formula(person, period, parameters): - p = parameters(period).gov.aca - age = person("monthly_age", period) - is_tax_dependent = person("is_tax_unit_dependent", period) - under_child_only_age = age <= p.slcsp.max_child_age - under_age_threshold = age < p.family_tier_dependent_child_age_threshold - ny_age_29_child = person("is_aca_ny_age_29_dependent_child", period) - return person("pays_aca_premium", period) & ( - under_child_only_age - | (is_tax_dependent & under_age_threshold) - | ny_age_29_child + return person("pays_aca_premium", period) & family_tier_child_condition( + person, period, parameters ) diff --git a/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py b/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py index cbd16a30ef5..2f925c9b6b0 100644 --- a/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py +++ b/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py @@ -1,6 +1,19 @@ from policyengine_us.model_api import * +def ny_age_29_child_condition(person, period, parameters): + """New York's age-29 dependent-child test without the premium condition.""" + p = parameters(period).gov.aca + state_code = person.household("state_code", period) + in_ny = state_code == state_code.possible_values.NY + age = person("monthly_age", period) + lower_age = p.family_tier_dependent_child_age_threshold + upper_age = p.ny_age_29_dependent_child_age_threshold + in_age_range = (age >= lower_age) & (age < upper_age) + is_tax_dependent = person("is_tax_unit_dependent", period) + return in_ny & in_age_range & is_tax_dependent + + class is_aca_ny_age_29_dependent_child(Variable): value_type = bool entity = Person @@ -8,14 +21,6 @@ class is_aca_ny_age_29_dependent_child(Variable): definition_period = MONTH def formula(person, period, parameters): - p = parameters(period).gov.aca - state_code = person.household("state_code", period) - in_ny = state_code == state_code.possible_values.NY - age = person("monthly_age", period) - lower_age = p.family_tier_dependent_child_age_threshold - upper_age = p.ny_age_29_dependent_child_age_threshold - in_age_range = (age >= lower_age) & (age < upper_age) - is_tax_dependent = person("is_tax_unit_dependent", period) - return ( - person("pays_aca_premium", period) & in_ny & in_age_range & is_tax_dependent + return person("pays_aca_premium", period) & ny_age_29_child_condition( + person, period, parameters ) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py index 1b9a354fdd4..a61cc5b803e 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.is_aca_family_tier_dependent_child import ( + family_tier_child_condition, +) class FamilyTierCategory(Enum): @@ -27,7 +30,7 @@ def family_tier_category(tax_unit, period, parameters, enrolled): state_code = tax_unit.household("state_code_str", period) person = tax_unit.members - dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + dependent_child = enrolled & family_tier_child_condition(person, period, parameters) non_dependent_adult_count = tax_unit.sum(enrolled & ~dependent_child) anchored_child_count = tax_unit.sum(dependent_child) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py index 194ac280ae0..04227da99d9 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py @@ -1,4 +1,10 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.is_aca_family_tier_dependent_child import ( + family_tier_child_condition, +) +from policyengine_us.variables.gov.aca.slspc.is_aca_ny_age_29_dependent_child import ( + ny_age_29_child_condition, +) from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( FamilyTierCategory, ) @@ -46,10 +52,10 @@ def family_tier_multiplier(tax_unit, period, parameters, family_category, enroll ) person = tax_unit.members - dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + dependent_child = enrolled & family_tier_child_condition(person, period, parameters) adult_count = tax_unit.sum(enrolled & ~dependent_child) extra_adults = max_(adult_count - 2, 0) - age_29_child = enrolled & person("is_aca_ny_age_29_dependent_child", period) + age_29_child = enrolled & ny_age_29_child_condition(person, period, parameters) age_29_multiplier = where( tax_unit.any(age_29_child), parameters(period).gov.aca.ny_age_29_dependent_child_tier_multiplier, From 4a97b84d7da9e387f05b97c50ea2bd91eb041ed2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 21:09:09 -0400 Subject: [PATCH 23/32] Correct PTC test fixtures after merging main --- .../tests/core/test_claimable_filer_ptc_invariants.py | 5 +++++ .../gov/aca/eligibility/is_aca_marketplace_enrollee.yaml | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py index 134745ca608..bfc54098f8c 100644 --- a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -416,5 +416,10 @@ def test_claimable_filer_ptc_seeded_population(year): + _ptc_cases(STATES) + _review_r2_cases() ) + # Explicit inputs apply simulation-wide, so the review cases' claimability + # inputs would otherwise default the legacy fixtures to false. + units = [ + {**unit, "claimable": unit.get("claimable", unit["claimed"])} for unit in units + ] check_swap(units, year, OUTPUTS, require_coverage=True) check_monotone(units, year, require_removal=True) diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml index b8dee264d14..233ab78b499 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml @@ -52,7 +52,8 @@ tax_unit: members: [person_a, person_b, child, child2, child3, child4] filing_status: JOINT - aca_magi: 40_000 + # Above the six-person poverty line so the coverage gap does not bind. + aca_magi: 50_000 tax_unit_is_filer: true households: household: From 93d53a8b1d10e5289d6cf249bd8371d66f4041af Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 21:34:19 -0400 Subject: [PATCH 24/32] Supply consistent premium and enrollment test inputs --- .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 68 ++++++++++++------- .../baseline/gov/aca/ptc/used_aca_ptc.yaml | 12 ++-- .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 28 ++++++-- .../aca_ptc_slcsp_family_tier_category.yaml | 24 +++++++ .../aca_ptc_slcsp_family_tier_multiplier.yaml | 18 +++++ .../co_premium_assistance.yaml | 12 ++-- .../nm_premium_assistance.yaml | 12 ++-- .../nm_premium_assistance_mih_eligible.yaml | 12 ++-- 8 files changed, 140 insertions(+), 46 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 028ba5b88da..7f5ca3d972d 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -289,7 +289,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -298,7 +298,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -311,6 +311,8 @@ household: members: [person_a, person_b] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: # 150% FPL for one, 0% contribution: 6,000 - 0 aca_tax_family_size: 1 @@ -328,7 +330,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -338,7 +340,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -351,6 +353,8 @@ household: members: [person_b, person_a] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: aca_tax_family_size: 1 aca_ptc_slcsp: 6000 @@ -469,7 +473,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -478,7 +482,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -491,6 +495,8 @@ household: members: [person_a, person_b] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: # 150% FPL for one, 4.19% contribution: 6,000 - 0.0419 * 23,475 aca_tax_family_size: 1 @@ -508,7 +514,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -518,7 +524,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -531,6 +537,8 @@ household: members: [person_b, person_a] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: aca_tax_family_size: 1 aca_ptc_slcsp: 6000 @@ -649,7 +657,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -658,7 +666,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -671,6 +679,8 @@ household: members: [person_a, person_b] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: # 18,000 / 15,060 = 119% FPL for a family of one: 0% contribution. aca_ptc: 6000 @@ -686,7 +696,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -696,7 +706,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -709,6 +719,8 @@ household: members: [person_b, person_a] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: aca_ptc: 6000 @@ -760,7 +772,7 @@ is_medicare_eligible: false claimable_as_dependent_on_another_return: true employment_income: 10_000 - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -770,7 +782,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false employment_income: 10_000 - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -780,7 +792,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false tax_exempt_interest_income: 20_000 - slcsp_age_curve_amount_person: 3000 + slcsp_age_curve_multiplier: 0.5 tax_units: tax_unit: members: [person_a, person_b, child] @@ -790,6 +802,8 @@ household: members: [person_a, person_b, child] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: # 20,000 / 15,060 = 132% FPL: 0% contribution; benchmark 6,000 (child out). aca_magi: 20_000 @@ -808,7 +822,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false employment_income: 10_000 - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -819,7 +833,7 @@ is_medicare_eligible: false claimable_as_dependent_on_another_return: true employment_income: 10_000 - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -829,7 +843,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false tax_exempt_interest_income: 20_000 - slcsp_age_curve_amount_person: 3000 + slcsp_age_curve_multiplier: 0.5 tax_units: tax_unit: members: [person_b, person_a, child] @@ -839,6 +853,8 @@ household: members: [person_b, person_a, child] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: aca_magi: 20_000 aca_magi_fraction: 1.32 @@ -857,7 +873,7 @@ is_medicare_eligible: false claimable_as_dependent_on_another_return: true dependent_claimant_filing_exception: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -866,7 +882,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -875,7 +891,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 3000 + slcsp_age_curve_multiplier: 0.5 tax_units: tax_unit: members: [person_a, person_b, child] @@ -886,6 +902,8 @@ household: members: [person_a, person_b, child] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: # 30,000 / 20,440 = 146% FPL: 0% contribution; benchmark 6,000 + 3,000. aca_tax_family_size: 2 @@ -903,7 +921,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -914,7 +932,7 @@ is_medicare_eligible: false claimable_as_dependent_on_another_return: true dependent_claimant_filing_exception: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -923,7 +941,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 3000 + slcsp_age_curve_multiplier: 0.5 tax_units: tax_unit: members: [person_b, person_a, child] @@ -934,6 +952,8 @@ household: members: [person_b, person_a, child] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: aca_tax_family_size: 2 aca_ptc_slcsp: 9000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml index 776d01b85dd..8abcd810034 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml @@ -87,7 +87,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -96,7 +96,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -110,6 +110,8 @@ household: members: [person_a, person_b] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: # min(6,000, 4,500); before, the cap was the whole 9,000 premium. aca_ptc: 6000 @@ -126,7 +128,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -136,7 +138,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -150,6 +152,8 @@ household: members: [person_b, person_a] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: aca_ptc: 6000 used_aca_ptc: 4500 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml index 5a70e1a382f..45d255e04bd 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -9,7 +9,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -18,7 +18,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -27,7 +27,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 3000 + slcsp_age_curve_multiplier: 0.5 tax_units: tax_unit: members: [person_a, person_b, child] @@ -37,6 +37,8 @@ household: members: [person_a, person_b, child] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: # 26 CFR 1.36B-3(f): the benchmark offered to the coverage family; # slcsp still counts every enrollee. @@ -53,7 +55,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -63,7 +65,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -72,7 +74,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_amount_person: 3000 + slcsp_age_curve_multiplier: 0.5 tax_units: tax_unit: members: [person_b, person_a, child] @@ -82,6 +84,8 @@ household: members: [person_b, person_a, child] state_code: TX + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 output: slcsp: 15_000 aca_ptc_slcsp: 6000 @@ -232,6 +236,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -239,12 +244,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -269,12 +276,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_a: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -282,6 +291,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_b, person_a, child] @@ -306,6 +316,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -313,12 +324,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -343,12 +356,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_a: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -356,6 +371,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_b, person_a, child] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml index a2269f7968d..2e7b08c27a7 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml @@ -6,6 +6,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -13,12 +14,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -42,12 +45,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_a: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -55,6 +60,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_b, person_a, child] @@ -78,6 +84,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -85,6 +92,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -92,6 +100,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -114,18 +123,21 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -149,6 +161,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -156,12 +169,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -185,12 +200,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_a: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -198,6 +215,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_b, person_a, child] @@ -221,6 +239,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -228,6 +247,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -235,6 +255,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -257,18 +278,21 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml index 427bf1091c7..d69e4eb7e50 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml @@ -6,6 +6,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -13,12 +14,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -42,12 +45,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_a: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -55,6 +60,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_b, person_a, child] @@ -78,6 +84,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -85,6 +92,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -92,6 +100,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -114,6 +123,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -121,12 +131,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: false is_tax_unit_spouse: true child: age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] @@ -150,12 +162,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} is_tax_unit_head: true is_tax_unit_spouse: false person_a: age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -163,6 +177,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_b, person_a, child] @@ -186,6 +201,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -193,6 +209,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -200,6 +217,7 @@ age: 5 monthly_age: 5 pays_aca_premium: true + is_aca_marketplace_enrollee: {2025: true} tax_units: tax_unit: members: [person_a, person_b, child] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml index 0b311e29096..d8de5e7de74 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml @@ -856,7 +856,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -867,7 +867,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -880,6 +880,8 @@ household: members: [person_a, person_b] state_code: CO + # Annual base: each adult has a 6,000 premium. + slcsp_age_0: 6000 output: # 20,000 / 15,650 = 127% FPL, 2.1%: contribution 420, aca_ptc 5,580. # min(12 * 80, 6,000 - 5,580) = 420; the whole-enrollment benchmark @@ -899,7 +901,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -909,7 +911,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -923,6 +925,8 @@ household: members: [person_b, person_a] state_code: CO + # Annual base: each adult has a 6,000 premium. + slcsp_age_0: 6000 output: aca_ptc: 5580 co_premium_assistance: 420 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml index 08d93ff99b2..ed403527136 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml @@ -900,7 +900,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -911,7 +911,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -924,6 +924,8 @@ household: members: [person_a, person_b] state_code: NM + # Annual base: each adult has a 6,000 premium. + slcsp_age_0: 6000 output: # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. @@ -943,7 +945,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -953,7 +955,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -967,6 +969,8 @@ household: members: [person_b, person_a] state_code: NM + # Annual base: each adult has a 6,000 premium. + slcsp_age_0: 6000 output: nm_premium_assistance: 1583.6 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml index 0ed3955268b..ee402a91e57 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml @@ -275,7 +275,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false @@ -286,7 +286,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -299,6 +299,8 @@ household: members: [person_a, person_b] state_code: NM + # Annual base: each adult has a 6,000 premium. + slcsp_age_0: 6000 output: # 70,000 / 15,650 = 447% FPL; 6,000 - 0.085 * 70,000 = 50. aca_magi_fraction: 4.47 @@ -317,7 +319,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -327,7 +329,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - slcsp_age_curve_amount_person: 6000 + slcsp_age_curve_multiplier: 1 claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true @@ -341,6 +343,8 @@ household: members: [person_b, person_a] state_code: NM + # Annual base: each adult has a 6,000 premium. + slcsp_age_0: 6000 output: aca_magi_fraction: 4.47 nm_premium_assistance_mih_eligible: true From ea2fd7e8fafe863778797c7dd3dc97dd23fa1da3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 21:56:33 -0400 Subject: [PATCH 25/32] Set Marketplace enrollment in New Mexico MIH test fixtures --- .../premium_assistance/nm_premium_assistance.yaml | 6 ++++++ .../nm_premium_assistance_mih_eligible.yaml | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml index ed403527136..a604bc63743 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml @@ -191,6 +191,7 @@ age: 40 is_aca_ptc_eligible: false pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -429,6 +430,7 @@ age: 40 is_aca_ptc_eligible: false pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -460,10 +462,12 @@ age: 45 is_aca_ptc_eligible: false pays_aca_premium: true + is_aca_marketplace_enrollee: true person2: age: 43 is_aca_ptc_eligible: false pays_aca_premium: false + is_aca_marketplace_enrollee: false tax_units: tax_unit: members: [person1, person2] @@ -612,6 +616,7 @@ age: 40 is_aca_ptc_eligible: false pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -821,6 +826,7 @@ age: 40 is_aca_ptc_eligible: false pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml index ee402a91e57..4526bf7a316 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml @@ -17,6 +17,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -39,6 +40,7 @@ person1: age: 40 pays_aca_premium: false + is_aca_marketplace_enrollee: false tax_units: tax_unit: members: [person1] @@ -61,9 +63,11 @@ person1: age: 45 pays_aca_premium: true + is_aca_marketplace_enrollee: true person2: age: 43 pays_aca_premium: false + is_aca_marketplace_enrollee: false tax_units: tax_unit: members: [person1, person2] @@ -86,9 +90,11 @@ person1: age: 45 pays_aca_premium: true + is_aca_marketplace_enrollee: true person2: age: 43 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1, person2] @@ -111,6 +117,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -133,6 +140,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -154,6 +162,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -176,6 +185,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -200,6 +210,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -225,6 +236,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] @@ -249,6 +261,7 @@ person1: age: 40 pays_aca_premium: true + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1] From 3ddc1270c29d7c933cccea50d37f6d6edb095210 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 23:22:09 -0400 Subject: [PATCH 26/32] Check CSR conditions for every Marketplace enrollee --- .../test_claimable_filer_ptc_invariants.py | 44 +++++- .../gov/aca/csr/marketplace_csr_eligible.yaml | 149 ++++++++++++++++++ .../gov/aca/csr/marketplace_csr_eligible.py | 12 +- .../aca/eligibility/is_aca_ptc_eligible.py | 25 +-- .../is_aca_ptc_eligible_before_premium.py | 38 +++++ 5 files changed, 238 insertions(+), 30 deletions(-) create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible_before_premium.py diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py index bfc54098f8c..d65b01c4cdb 100644 --- a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -27,8 +27,9 @@ the inputs, not from the model's family variables); used credit <= coverage-family premium <= selected premium; no family member means no credit and no coverage-family premium; cost-sharing reductions only when - every enrollee is eligible for the credit; and on a return with a head or - spouse where neither can be claimed, `aca_ptc_slcsp` = `slcsp`, + every enrollee meets the credit's conditions before premium payment; and + on a return with a head or spouse where neither can be claimed, + `aca_ptc_slcsp` = `slcsp`, `aca_magi` = `medicaid_magi`, `aca_tax_family_size` = `tax_unit_size` and `aca_fpg` = `tax_unit_fpg` for the prior year (these simulations supply no prior-year size, so the two sizes agree). A unit with neither a head nor a @@ -205,6 +206,32 @@ def _with_dependent_income(units): return out +def _review_r3_cases(): + # The 19-year-old spouse is an enrolled fourth ACA child, so pays no + # marginal premium. Claimability excludes that spouse from substantive + # PTC eligibility and blocks CSR. Ordinary fourth children remain + # substantively eligible despite also paying no marginal premium. + independent = {"age": 40, "employment_income": 50_000.0} + student = {"age": 20, "is_full_time_student": True} + return [ + { + "state": "TX", + "adults": [independent, {"age": 19}], + "dependents": [student, student, student], + "claimed": (False, False), + "claimable": (False, True), + "exception": (False, True), + }, + { + "state": "TX", + "adults": [independent, {"age": 30}], + "dependents": [student, student, student, {"age": 19}], + "claimed": (False, False), + "claimable": (False, False), + }, + ] + + def _tax_unit_sum(sim, name, year): return np.asarray(sim.calculate(name, year, map_to="tax_unit"), dtype=float) @@ -222,7 +249,9 @@ def _enrollment_signature(sim, year): # Which members are enrolled, as one number per tax unit, so variants # can be compared person by person rather than by count. position = np.asarray(sim.populations["tax_unit"].members_position) - enrolled = np.asarray(sim.calculate("pays_aca_premium", year), dtype=float) + enrolled = np.asarray( + sim.calculate("is_aca_marketplace_enrollee", year), dtype=float + ) return _tax_unit_sum_array(sim, enrolled * 2.0**position) @@ -294,8 +323,12 @@ def _identities(sim, year): assert (used <= family_premium + TOLERANCE).all() assert (family_premium <= premium + TOLERANCE).all() csr = _calc(sim, "marketplace_csr_eligible", year) > 0 - enrolled = np.asarray(sim.calculate("pays_aca_premium", year)) - ineligible_enrollees = _tax_unit_sum_array(sim, enrolled & ~eligible) + enrolled = np.asarray(sim.calculate("is_aca_marketplace_enrollee", year)) + substantive_eligible = np.asarray( + sim.calculate("is_aca_ptc_eligible_before_premium", year) + ) + assert not (substantive_eligible & ~member).any() + ineligible_enrollees = _tax_unit_sum_array(sim, enrolled & ~substantive_eligible) assert not (csr & (ineligible_enrollees > 0)).any() assert not _calc(sim, "aca_ptc", year)[size == 0].any() assert not family_premium[size == 0].any() @@ -415,6 +448,7 @@ def test_claimable_filer_ptc_seeded_population(year): + _crafted_cases(STATES, review_case_only=True) + _ptc_cases(STATES) + _review_r2_cases() + + _review_r3_cases() ) # Explicit inputs apply simulation-wide, so the review cases' claimability # inputs would otherwise default the legacy fixtures to false. diff --git a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml index de6bf23e9d8..ce5c042cea9 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml @@ -139,3 +139,152 @@ state_code: TX output: marketplace_csr_eligible: true + +- name: Review r3 2025 enrolled claimable spouse who pays no marginal premium blocks shared-policy CSR + period: 2025 + input: + people: + head: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + spouse: + age: 19 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child_1: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child_2: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child_3: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [head, spouse, child_1, child_2, child_3] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + selected_marketplace_plan_category: SILVER + households: + household: + members: [head, spouse, child_1, child_2, child_3] + state_code: TX + output: + # The spouse is fourth among the covered under-21s. The three-child + # premium cap does not remove them from the single policy's CSR test + # under 45 CFR 155.305(g)(3). + is_aca_marketplace_enrollee: [true, true, true, true, true] + pays_aca_premium: [true, false, true, true, true] + is_aca_tax_family_member: [true, false, true, true, true] + is_aca_ptc_eligible: [true, false, true, true, true] + marketplace_csr_eligible: false + +- name: Review r3 2025 ordinary fourth enrolled child who pays no marginal premium preserves shared-policy CSR + period: 2025 + input: + people: + head: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child_1: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child_2: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child_3: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child_4: + age: 19 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [head, child_1, child_2, child_3, child_4] + filing_status: HEAD_OF_HOUSEHOLD + aca_magi: 40_000 + tax_unit_is_filer: true + selected_marketplace_plan_category: SILVER + households: + household: + members: [head, child_1, child_2, child_3, child_4] + state_code: TX + output: + # If separately insured the fourth child would meet the same PTC + # conditions as the other family members; the premium cap changes only + # the marginal charge, not their eligibility under 155.305(g)(3). + is_aca_marketplace_enrollee: [true, true, true, true, true] + pays_aca_premium: [true, true, true, true, false] + is_aca_tax_family_member: [true, true, true, true, true] + is_aca_ptc_eligible: [true, true, true, true, false] + marketplace_csr_eligible: true diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py index 5d6718c052a..30cdb0235f5 100644 --- a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py +++ b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py @@ -19,8 +19,16 @@ class marketplace_csr_eligible(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members receives_aca = tax_unit.sum(person("person_receives_aca", period)) > 0 - enrollee = person("pays_aca_premium", period) - ptc_eligible = person("is_aca_ptc_eligible", period) + enrollee = person("is_aca_marketplace_enrollee", period) + # The rating cap can leave an insured child with no marginal + # premium. Check that enrollee's substantive conditions as though + # separately insured, as required by 45 CFR 155.305(g)(3). + # Preserve supplied PTC eligibility for those who do pay a premium. + ptc_eligible = where( + person("pays_aca_premium", period), + person("is_aca_ptc_eligible", period), + person("is_aca_ptc_eligible_before_premium", period), + ) every_enrollee_eligible = ~tax_unit.any(enrollee & ~ptc_eligible) category = tax_unit("selected_marketplace_plan_category", period) silver_selected = category == category.possible_values.SILVER diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py index 88cdd68742d..7338e9d1816 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py @@ -12,27 +12,6 @@ class is_aca_ptc_eligible(Variable): ) def formula(person, period, parameters): - fstatus = person.tax_unit("filing_status", period) - separate = fstatus == fstatus.possible_values.SEPARATE - - # determine income eligibility for ACA PTC - p = parameters(period).gov.aca - magi_frac = person.tax_unit("aca_magi_fraction", period) - standard_income_eligible = p.ptc_income_eligibility.calc(magi_frac) - below_fpl_exception = person.tax_unit( - "aca_ptc_below_fpl_immigration_exception", period - ) - is_income_eligible = standard_income_eligible | below_fpl_exception - - # No credit is allowed to someone another taxpayer can claim (26 - # U.S.C. 36B(c)(1)(D)); they are not an applicable taxpayer (26 CFR - # 1.36B-2(b)(3)) and are outside the tax family, along with the - # dependents of a return that has such a filer (IRC 152(b)(1)). - tax_family_member = person("is_aca_tax_family_member", period) - - return ( - person("pays_aca_premium", period) - & tax_family_member - & ~separate - & is_income_eligible + return person("pays_aca_premium", period) & person( + "is_aca_ptc_eligible_before_premium", period ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible_before_premium.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible_before_premium.py new file mode 100644 index 00000000000..f23a1b0ae22 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible_before_premium.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class is_aca_ptc_eligible_before_premium(Variable): + value_type = bool + entity = Person + label = "Person meets ACA premium tax credit conditions before premium payment" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#c_1", + "https://www.law.cornell.edu/cfr/text/26/1.36B-2#b", + ) + documentation = ( + "The tax-family, filing-status and income conditions for premium tax " + "credit eligibility, before the enrollment and premium-payment " + "conditions. An enrolled fourth child can meet these conditions " + "even though the three-child rating limit means no marginal premium " + "is charged. These conditions therefore apply to every enrollee " + "when determining shared-policy cost-sharing reductions." + ) + + def formula(person, period, parameters): + fstatus = person.tax_unit("filing_status", period) + separate = fstatus == fstatus.possible_values.SEPARATE + + p = parameters(period).gov.aca + magi_frac = person.tax_unit("aca_magi_fraction", period) + standard_income_eligible = p.ptc_income_eligibility.calc(magi_frac) + below_fpl_exception = person.tax_unit( + "aca_ptc_below_fpl_immigration_exception", period + ) + is_income_eligible = standard_income_eligible | below_fpl_exception + + # Someone another taxpayer can claim is not an applicable taxpayer + # (IRC 36B(c)(1)(D), 26 CFR 1.36B-2(b)(3)). The family variable also + # applies the dependent-taxpayer rule in IRC 152(b)(1). + tax_family_member = person("is_aca_tax_family_member", period) + return tax_family_member & ~separate & is_income_eligible From dcfc03cb09f89af6da8a0934159652c6da3bf5a1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 23:47:56 -0400 Subject: [PATCH 27/32] Preserve ACA MAGI inputs and infer minor joint filers --- .../gov/aca/eligibility/aca_magi.yaml | 45 +++++ .../eligibility/is_aca_tax_family_member.yaml | 170 ++++++++++++++++++ .../variables/gov/aca/eligibility/aca_magi.py | 20 ++- .../gov/aca/eligibility/is_aca_filer.py | 35 ++++ .../eligibility/is_aca_tax_family_member.py | 35 ++-- 5 files changed, 280 insertions(+), 25 deletions(-) create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml index d2bb20d5090..5be0b7aa513 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml @@ -338,3 +338,48 @@ output: medicaid_magi: 0 aca_magi: 20_000 + +- name: Review r3 supplied Medicaid MAGI remains ACA MAGI without excluded dependents + period: 2025 + input: + age: 30 + state_code: TX + medicaid_magi: 20_000 + taxpayer_has_tin: true + is_aca_ptc_immigration_status_eligible: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + aca_ptc_below_fpl_immigration_exception: false + output: + aca_magi: 20_000 + pays_aca_premium: true + +- name: Review r3 minor joint filers both retain their MAGI additions + period: 2025 + input: + people: + older_spouse: + age: 17 + claimable_as_dependent_on_another_return: true + tax_exempt_interest_income: 2_000 + younger_spouse: + age: 16 + tax_exempt_interest_income: 3_000 + child: + age: 1 + tax_exempt_interest_income: 4_000 + tax_units: + tax_unit: + members: [older_spouse, younger_spouse, child] + filing_status: JOINT + adjusted_gross_income: 20_000 + households: + household: + members: [older_spouse, younger_spouse, child] + state_code: TX + output: + # Both spouses' additions remain; only the excluded child's 4,000 leaves. + medicaid_magi: 29_000 + aca_magi: 25_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml index 4e7fbfc067e..0b306fec450 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -497,3 +497,173 @@ state_code: TX output: is_aca_tax_family_member: [true, true] + +# Ages alone cannot distinguish equally old filers from dependents; +# the explicit-role cases document how callers resolve that ambiguity. +- name: Review r3 minor joint filers ages 17 and 16, order ['a', 'b'] + period: 2025 + input: + people: + a: + age: 17 + claimable_as_dependent_on_another_return: true + b: + age: 16 + tax_units: + tax_unit: + members: &id001 + - a + - b + filing_status: JOINT + households: + household: + members: *id001 + state_code: TX + output: + is_aca_tax_family_member: + - false + - true +- name: Review r3 minor joint filers ages 17 and 17, order ['a', 'b'] + period: 2025 + input: + people: + a: + age: 17 + claimable_as_dependent_on_another_return: true + b: + age: 17 + tax_units: + tax_unit: + members: &id002 + - a + - b + filing_status: JOINT + households: + household: + members: *id002 + state_code: TX + output: + is_aca_tax_family_member: + - false + - true +- name: Review r3 minor joint filers ages 17 and 17, order ['b', 'a'] + period: 2025 + input: + people: + b: + age: 17 + a: + age: 17 + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: &id003 + - b + - a + filing_status: JOINT + households: + household: + members: *id003 + state_code: TX + output: + is_aca_tax_family_member: + - true + - false +- name: Review r3 equal-age minor filers and dependent with explicit roles, order + ['a', 'b', 'dependent'] + period: 2025 + input: + people: + a: + age: 17 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + b: + age: 17 + is_tax_unit_head: false + is_tax_unit_spouse: true + dependent: + age: 17 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: &id004 + - a + - b + - dependent + filing_status: JOINT + households: + household: + members: *id004 + state_code: TX + output: + is_aca_tax_family_member: + - false + - true + - false +- name: Review r3 equal-age minor filers and dependent with explicit roles, order + ['b', 'a', 'dependent'] + period: 2025 + input: + people: + b: + age: 17 + is_tax_unit_head: false + is_tax_unit_spouse: true + a: + age: 17 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + dependent: + age: 17 + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: &id005 + - b + - a + - dependent + filing_status: JOINT + households: + household: + members: *id005 + state_code: TX + output: + is_aca_tax_family_member: + - true + - false + - false +- name: Review r3 minor joint filer inference respects supplied dependency hints + period: 2025 + input: + people: + dependent: + age: 17 + is_tax_unit_dependent: true + a: + age: 16 + is_tax_unit_dependent: false + claimable_as_dependent_on_another_return: true + b: + age: 15 + is_tax_unit_dependent: false + tax_units: + tax_unit: + members: &id006 + - dependent + - a + - b + filing_status: JOINT + households: + household: + members: *id006 + state_code: TX + output: + is_aca_tax_family_member: + - false + - false + - true diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py index 118e4f16533..61bb6dff35d 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py @@ -20,18 +20,18 @@ class aca_magi(Variable): "outside the tax family (is_aca_tax_family_member), floored at zero " "once. Those amounts are left out only on a return whose head or " "spouse can be claimed elsewhere (or, in a tax unit with neither, " - "whose oldest member can be); otherwise this equals medicaid_magi. " + "whose inferred filer can be); otherwise this equals medicaid_magi. " "Tax unit AGI already leaves dependents' income out." ) def formula(tax_unit, period, parameters): person = tax_unit.members - filer = person("is_tax_unit_head_or_spouse", period) - has_filer = person.tax_unit.any(filer) - age = person("age", period) - oldest = person.get_rank(tax_unit, -age) == 0 - files_return = where(has_filer, filer, oldest) + files_return = person("is_aca_filer", period) outside_family = ~files_return & ~person("is_aca_tax_family_member", period) + needs_exclusion = tax_unit.any(outside_family) + medicaid_magi = tax_unit("medicaid_magi", period) + if not needs_exclusion.any(): + return medicaid_magi excluded = tax_unit.sum( outside_family * ( @@ -45,4 +45,10 @@ def formula(tax_unit, period, parameters): unfloored = tax_unit("adjusted_gross_income", period) + add( tax_unit, period, additions ) - return max_(0, unfloored - excluded) + # Keep supplied Medicaid MAGI when no dependent is excluded. Only + # reconstruct the unfloored amount when removing family income. + return where( + needs_exclusion, + max_(0, unfloored - excluded), + medicaid_magi, + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py new file mode 100644 index 00000000000..27a2974c4b1 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class is_aca_filer(Variable): + value_type = bool + entity = Person + label = "Filer or joint filer for ACA tax family calculations" + definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d" + documentation = ( + "Uses the tax unit's head and spouse when either role exists. When " + "adult role inference leaves both absent, the oldest member files, " + "or the two oldest on a joint return. Supplied tax-unit dependency " + "hints exclude dependents from this fallback when possible. Ages " + "alone cannot distinguish a filer from an equally old dependent; " + "supply explicit head/spouse roles for such ambiguous households. " + "This ACA-only inference does not change the tax unit's other roles." + ) + + def formula(person, period, parameters): + tax_unit = person.tax_unit + filer = person("is_tax_unit_head_or_spouse", period) + has_filer = tax_unit.any(filer) + # Read the stored hint without deriving dependency from the roles we + # are filling in. If every member is marked dependent (including a + # cached role-derived array for minors), retain the age fallback. + dependent = person.simulation.get_array("is_tax_unit_dependent", period) + candidate = person.filled_array(True) + if dependent is not None: + candidate = where(tax_unit.any(~dependent), ~dependent, candidate) + rank = person.get_rank(tax_unit, -person("age", period), candidate) + status = tax_unit("filing_status", period) + joint = status == status.possible_values.JOINT + inferred = candidate & (rank < (1 + joint)) + return where(has_filer, filer, inferred) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py index 48294317543..8394d236604 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -30,30 +30,29 @@ class is_aca_tax_family_member(Variable): "and spouse can be claimed has no applicable taxpayer and no family " "for the credit. A tax unit with neither a head nor a spouse (role " "inference makes the oldest adult the head, so a unit of minors has " - "none) is treated as filed by its oldest member: everyone is in the " - "family if that member cannot be claimed elsewhere, and nobody is if " - "they can. A " + "none) uses is_aca_filer to identify one filer, or two on a joint " + "return, using age and supplied dependency hints. Explicit roles are " + "needed when ages cannot distinguish filers from dependents. A " "dependent on a return with an applicable taxpayer is a member even if " "another taxpayer could also claim them: this return claims them." ) def formula(person, period, parameters): - filer = person("is_tax_unit_head_or_spouse", period) + filer = person("is_aca_filer", period) claimable = person("claimable_as_dependent_on_another_return", period) - has_filer = person.tax_unit.any(filer) - applicable_taxpayer = ( - person.tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 + has_roles = person.tax_unit.any(person("is_tax_unit_head_or_spouse", period)) + applicable_taxpayer = where( + has_roles, + person.tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0, + person.tax_unit.any(filer & ~claimable), ) - claims_dependents = ~person.tax_unit( - "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + exception = person("dependent_claimant_filing_exception", period) + claims_dependents = ~where( + has_roles, + person.tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ), + person.tax_unit.any(filer & claimable & ~exception), ) dependent_member = applicable_taxpayer & claims_dependents - # No head or spouse: the oldest member files. - age = person("age", period) - oldest = person.get_rank(person.tax_unit, -age) == 0 - oldest_claimable = person.tax_unit.any(oldest & claimable) - return where( - has_filer, - where(filer, ~claimable, dependent_member), - ~oldest_claimable, - ) + return where(filer, ~claimable, dependent_member) From 930ee0ccae0e9bc4d77a5e80a363b542ebb0c4c7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 00:00:20 -0400 Subject: [PATCH 28/32] Preserve supplied ACA prices and rank covered children --- .../gov/aca/eligibility/aca_child_index.yaml | 32 ++++ .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 171 +++++++++++++++++- .../gov/aca/slcsp/family_tier_category.yaml | 45 +++++ .../gov/aca/slcsp/family_tier_multiplier.yaml | 47 +++++ .../gov/aca/eligibility/aca_child_index.py | 15 +- .../gov/aca/eligibility/pays_aca_premium.py | 6 +- .../variables/gov/aca/slspc/aca_ptc_slcsp.py | 8 +- .../is_aca_family_tier_dependent_child.py | 19 +- .../slcsp_age_curve_amount_person_uncapped.py | 47 +++++ .../aca/slspc/slcsp_family_tier_category.py | 6 +- .../aca/slspc/slcsp_family_tier_multiplier.py | 11 +- 11 files changed, 373 insertions(+), 34 deletions(-) create mode 100644 policyengine_us/variables/gov/aca/slspc/slcsp_age_curve_amount_person_uncapped.py diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_child_index.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_child_index.yaml index 97336eff616..89a04c5e49c 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_child_index.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_child_index.yaml @@ -4,18 +4,50 @@ people: person1: age: 40 + is_aca_marketplace_enrollee: true person2: age: 39 + is_aca_marketplace_enrollee: true person3: age: 14 + is_aca_marketplace_enrollee: true person4: age: 13 + is_aca_marketplace_enrollee: true person5: age: 12 + is_aca_marketplace_enrollee: true person6: age: 12 + is_aca_marketplace_enrollee: true tax_units: tax_unit: members: [person1, person2, person3, person4, person5, person6] output: aca_child_index: [99, 99, 1, 2, 3, 4] + +- name: r3 only covered children take ACA child-premium ranking slots + period: 2025 + input: + people: + adult: + age: 30 + is_aca_marketplace_enrollee: true + child_17: + age: 17 + is_aca_marketplace_enrollee: false + child_16: + age: 16 + is_aca_marketplace_enrollee: false + child_15: + age: 15 + is_aca_marketplace_enrollee: false + child_10: + age: 10 + is_aca_marketplace_enrollee: true + tax_units: + tax_unit: + members: [adult, child_17, child_16, child_15, child_10] + output: + aca_child_index: [99, 99, 99, 99, 1] + pays_aca_premium: [true, false, false, false, true] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml index 45d255e04bd..047d188ecb4 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -9,7 +9,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_multiplier: 1 + slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -18,7 +18,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_multiplier: 1 + slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -27,7 +27,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_multiplier: 0.5 + slcsp_age_curve_amount_person: 3000 tax_units: tax_unit: members: [person_a, person_b, child] @@ -37,8 +37,6 @@ household: members: [person_a, person_b, child] state_code: TX - # Annual base: adults cost 6,000 and children cost 3,000. - slcsp_age_0: 6000 output: # 26 CFR 1.36B-3(f): the benchmark offered to the coverage family; # slcsp still counts every enrollee. @@ -55,7 +53,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_multiplier: 1 + slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false person_a: @@ -65,7 +63,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false claimable_as_dependent_on_another_return: true - slcsp_age_curve_multiplier: 1 + slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -74,7 +72,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - slcsp_age_curve_multiplier: 0.5 + slcsp_age_curve_amount_person: 3000 tax_units: tax_unit: members: [person_b, person_a, child] @@ -84,8 +82,6 @@ household: members: [person_b, person_a, child] state_code: TX - # Annual base: adults cost 6,000 and children cost 3,000. - slcsp_age_0: 6000 output: slcsp: 15_000 aca_ptc_slcsp: 6000 @@ -455,6 +451,161 @@ slcsp: 4713.8 aca_ptc_slcsp: 4483.7 +- name: r3 2025 TX older children with other coverage leave a child-premium slot for the enrolled youngest child + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + spouse: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child_17: + age: 17 + is_aca_eshi_eligible: true + child_16: + age: 16 + is_aca_eshi_eligible: true + child_15: + age: 15 + is_aca_eshi_eligible: true + child_10: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [head, spouse, child_17, child_16, child_15, child_10] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + households: + household: + members: [head, spouse, child_17, child_16, child_15, child_10] + state_code: TX + slcsp_age_0: 1000 + slcsp_rating_area: 1 + output: + is_aca_marketplace_enrollee: [true, true, false, false, false, true] + is_aca_tax_family_member: [true, false, true, true, true, true] + # 45 CFR 147.102(c)(1) counts only covered children. The adult's + # 1,483.70 and the covered 10-year-old's 1,000 both enter the benchmark. + pays_aca_premium: [true, true, false, false, false, true] + slcsp: 3967.40 + aca_ptc_slcsp: 2483.70 + +- name: r3 supplied age-curve premiums remain inputs to a mixed coverage-family benchmark + period: 2025 + input: + people: + head: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + spouse: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: TX + output: + slcsp: 12_000 + aca_ptc_slcsp: 6000 + +- name: r3 a supplied age-curve premium is preserved for an enrolled nonpayer + period: 2025 + input: + people: + head: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + pays_aca_premium: false + slcsp_age_curve_amount_person: 6000 + spouse: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: TX + output: + slcsp: 12_000 + aca_ptc_slcsp: 6000 + +- name: r3 a supplied zero age-curve premium is preserved for an enrolled nonpayer + period: 2025 + input: + people: + head: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + pays_aca_premium: false + slcsp_age_curve_amount_person: 0 + spouse: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: TX + output: + slcsp: 6000 + aca_ptc_slcsp: 0 + - name: 2025 coverage family's three oldest children are charged once a claimable 19-year-old spouse is out (the claimable spouse is the spouse) period: 2025 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_category.yaml index 8422bdb332f..6537e31ad00 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_category.yaml @@ -390,3 +390,48 @@ state_code_str: CA output: slcsp_family_tier_category: INDIVIDUAL_AGE_RATED + +- name: r3 supplied family-tier child predicates preserve the category and extra-adult premium + period: 2025-01 + input: + people: + head: + age: 40 + monthly_age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_aca_marketplace_enrollee: {2025: true} + pays_aca_premium: true + spouse: + age: 40 + monthly_age: 40 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + is_aca_marketplace_enrollee: {2025: true} + pays_aca_premium: true + dependent: + age: 25 + monthly_age: 25 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + pays_aca_premium: true + is_aca_family_tier_dependent_child: false + tax_units: + tax_unit: + members: [head, spouse, dependent] + filing_status: JOINT + households: + household: + members: [head, spouse, dependent] + state_code: NY + state_code_str: NY + slcsp_age_0: 500 + output: + slcsp_family_tier_category: TWO_ADULTS + slcsp_family_tier_multiplier: 3 + slcsp: 1500 + aca_ptc_slcsp: 1500 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_multiplier.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_multiplier.yaml index e1e0286643a..a857b10ad73 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_multiplier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/family_tier_multiplier.yaml @@ -184,3 +184,50 @@ state_fips: 50 output: slcsp_family_tier_multiplier: 2.81 + +- name: r3 a supplied false NY age-29 predicate preserves two-adult pricing with no + claimable filer + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + dependent: + age: 27 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + is_aca_ny_age_29_dependent_child: false + tax_units: + tax_unit: + members: + - head + - dependent + filing_status: SINGLE + households: + household: + members: + - head + - dependent + state_code: NY + state_code_str: NY + slcsp_age_0: 6000 + output: + slcsp: 12000 + is_aca_family_tier_dependent_child: + 2025-01: + - false + - false + slcsp_family_tier_category: + 2025-01: TWO_ADULTS + slcsp_family_tier_multiplier: + 2025-01: 2 + aca_ptc_slcsp: 12000 diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_child_index.py b/policyengine_us/variables/gov/aca/eligibility/aca_child_index.py index 1beb508a028..725018254c6 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_child_index.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_child_index.py @@ -4,13 +4,20 @@ class aca_child_index(Variable): value_type = int entity = Person - label = "Index of child in tax unit (1 = oldest)" + label = "Index of covered ACA child in tax unit (1 = oldest)" definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/45/147.102#c_1" + documentation = ( + "Rank Marketplace-enrolled children by age, with 1 for the oldest. " + "Only covered children count toward the three-child premium limit. " + "Adults and people not enrolled receive the sentinel value 99." + ) def formula(person, period, parameters): age = person("age", period) is_child = age <= parameters(period).gov.aca.slcsp.max_child_age - child_rank = person.get_rank(person.tax_unit, -age, condition=is_child) + covered_child = is_child & person("is_aca_marketplace_enrollee", period) + child_rank = person.get_rank(person.tax_unit, -age, condition=covered_child) # child_rank of 0 ==> oldest; ties broken by order in the tax unit - ADULT_VALUE = 99 - return where(is_child, child_rank + 1, ADULT_VALUE) + NOT_COVERED_CHILD_VALUE = 99 + return where(covered_child, child_rank + 1, NOT_COVERED_CHILD_VALUE) diff --git a/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py b/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py index 044d4f01591..ee13d6d755e 100644 --- a/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py +++ b/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py @@ -25,13 +25,13 @@ class pays_aca_premium(Variable): "income (policyengine-us #9472). A reader that wants the status and " "coverage conditions without that assumption should not reuse this " "variable. It is is_aca_marketplace_enrollee restricted to people " - "charged an age-rated premium: adults, and the three oldest children " - "in the tax unit." + "charged an age-rated premium: adults, and the three oldest " + "enrolled children in the tax unit." ) def formula(person, period, parameters): # An enrollee pays an age-rated premium unless they are a child - # beyond the three oldest (45 CFR 147.102(c)(1)). + # beyond the three oldest covered children (45 CFR 147.102(c)(1)). enrollee = person("is_aca_marketplace_enrollee", period) p = parameters(period).gov.aca is_aca_adult = person("age", period) > p.slcsp.max_child_age diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py index a49a4df22a1..94e999c8ab7 100644 --- a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py @@ -20,12 +20,10 @@ def benchmark_premium(tax_unit, period, parameters, group, enrolled): p = parameters(period).gov.aca age = person("age", period.this_year) is_child = age <= p.slcsp.max_child_age - child_rank = person.get_rank(tax_unit, -age, condition=group & is_child) + child_rank = person.get_rank(tax_unit, -age, condition=group & enrolled & is_child) charged = group & enrolled & (~is_child | (child_rank < p.max_child_count)) - base = person.household("slcsp_age_0", period) - multiplier = person("slcsp_age_curve_multiplier", period) - age_curve_applies = tax_unit("slcsp_age_curve_applies", period) - age_curve = tax_unit.sum(charged * base * multiplier) * age_curve_applies + amount = person("slcsp_age_curve_amount_person_uncapped", period) + age_curve = tax_unit.sum(charged * amount) tier_enrolled = group & enrolled category = family_tier_category(tax_unit, period, parameters, tier_enrolled) tier_multiplier = family_tier_multiplier( diff --git a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py index 82b44cf6cee..c23adfbe551 100644 --- a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py +++ b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py @@ -1,7 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.aca.slspc.is_aca_ny_age_29_dependent_child import ( - ny_age_29_child_condition, -) def family_tier_child_condition(person, period, parameters): @@ -14,7 +11,21 @@ def family_tier_child_condition(person, period, parameters): return ( under_child_only_age | (is_tax_dependent & under_age_threshold) - | ny_age_29_child_condition(person, period, parameters) + | person("is_aca_ny_age_29_dependent_child", period) + ) + + +def family_tier_child_condition_for_enrollee(person, period, parameters): + """Preserve supplied child classifications when repricing enrollees. + + The public predicate includes premium payment. Use its supplied or + calculated value for payers, and apply the uncapped child condition to + enrolled children who add no age-rated premium to the whole policy. + """ + return where( + person("pays_aca_premium", period.this_year), + person("is_aca_family_tier_dependent_child", period), + family_tier_child_condition(person, period, parameters), ) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_age_curve_amount_person_uncapped.py b/policyengine_us/variables/gov/aca/slspc/slcsp_age_curve_amount_person_uncapped.py new file mode 100644 index 00000000000..88ca163cdbb --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_age_curve_amount_person_uncapped.py @@ -0,0 +1,47 @@ +from policyengine_us.model_api import * + + +class slcsp_age_curve_amount_person_uncapped(Variable): + value_type = float + entity = Person + label = "Second-lowest ACA silver-plan cost before the three-child premium cap" + unit = USD + definition_period = MONTH + reference = "https://www.law.cornell.edu/cfr/text/45/147.102#c_1" + documentation = ( + "The person's age-rated benchmark before limiting a family's charges " + "to its three oldest covered children. For premium payers, read " + "slcsp_age_curve_amount_person directly. Supplied premiums, including " + "zero, carry through regardless of premium-payment status. A child " + "who adds no premium to the whole policy may add " + "one when a different coverage family is priced, so derive that " + "person's uncapped quote separately." + ) + + def formula(person, period, parameters): + simulation = person.simulation + name = "slcsp_age_curve_amount_person" + if hasattr(simulation, "_get_set_input_periods"): + supplied = period in simulation._get_set_input_periods(name) + else: + # Older Core releases expose the constructor inputs and later + # set_input provenance separately. + branches = ( + simulation._get_visible_branch_names() + if hasattr(simulation, "_get_visible_branch_names") + else {getattr(simulation, "branch_name", "default"), "default"} + ) + supplied = name in simulation.input_variables or any( + variable == name and branch in branches and set_period == period + for variable, branch, set_period in ( + getattr(simulation, "_user_input_keys", None) or () + ) + ) + if supplied: + return person(name, period) + amount = person("slcsp_age_curve_amount_person", period) + pays_premium = person("pays_aca_premium", period.this_year) + base = person.household("slcsp_age_0", period) + multiplier = person("slcsp_age_curve_multiplier", period) + age_curve_applies = person.tax_unit("slcsp_age_curve_applies", period) + return where(pays_premium, amount, base * multiplier * age_curve_applies) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py index a61cc5b803e..738f16caa2b 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py @@ -1,6 +1,6 @@ from policyengine_us.model_api import * from policyengine_us.variables.gov.aca.slspc.is_aca_family_tier_dependent_child import ( - family_tier_child_condition, + family_tier_child_condition_for_enrollee, ) @@ -30,7 +30,9 @@ def family_tier_category(tax_unit, period, parameters, enrolled): state_code = tax_unit.household("state_code_str", period) person = tax_unit.members - dependent_child = enrolled & family_tier_child_condition(person, period, parameters) + dependent_child = enrolled & family_tier_child_condition_for_enrollee( + person, period, parameters + ) non_dependent_adult_count = tax_unit.sum(enrolled & ~dependent_child) anchored_child_count = tax_unit.sum(dependent_child) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py index 04227da99d9..28a4b8c7645 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py @@ -1,9 +1,6 @@ from policyengine_us.model_api import * from policyengine_us.variables.gov.aca.slspc.is_aca_family_tier_dependent_child import ( - family_tier_child_condition, -) -from policyengine_us.variables.gov.aca.slspc.is_aca_ny_age_29_dependent_child import ( - ny_age_29_child_condition, + family_tier_child_condition_for_enrollee, ) from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( FamilyTierCategory, @@ -52,10 +49,12 @@ def family_tier_multiplier(tax_unit, period, parameters, family_category, enroll ) person = tax_unit.members - dependent_child = enrolled & family_tier_child_condition(person, period, parameters) + dependent_child = enrolled & family_tier_child_condition_for_enrollee( + person, period, parameters + ) adult_count = tax_unit.sum(enrolled & ~dependent_child) extra_adults = max_(adult_count - 2, 0) - age_29_child = enrolled & ny_age_29_child_condition(person, period, parameters) + age_29_child = enrolled & person("is_aca_ny_age_29_dependent_child", period) age_29_multiplier = where( tax_unit.any(age_29_child), parameters(period).gov.aca.ny_age_29_dependent_child_tier_multiplier, From 4d20fc9a312cbc45f5f5da024186bf3ef1978a40 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 00:06:36 -0400 Subject: [PATCH 29/32] Allocate shared ACA premiums by coverage month --- changelog.d/claimable-filer-ptc.fixed.md | 2 + .../ptc/aca_ptc_coverage_family_premium.yaml | 218 ++++++++++++++++++ .../gov/aca/ptc/marketplace_net_premium.yaml | 2 + .../baseline/gov/aca/ptc/used_aca_ptc.yaml | 3 + .../ptc/aca_ptc_coverage_family_premium.py | 51 ++-- ...selected_marketplace_plan_premium_proxy.py | 35 +++ ...selected_marketplace_plan_premium_proxy.py | 20 +- 7 files changed, 299 insertions(+), 32 deletions(-) create mode 100644 policyengine_us/variables/gov/aca/ptc/monthly_selected_marketplace_plan_premium_proxy.py diff --git a/changelog.d/claimable-filer-ptc.fixed.md b/changelog.d/claimable-filer-ptc.fixed.md index 00c137474dd..b65107abedf 100644 --- a/changelog.d/claimable-filer-ptc.fixed.md +++ b/changelog.d/claimable-filer-ptc.fixed.md @@ -1 +1,3 @@ Apply IRC 36B(c)(1)(D) to the premium tax credit: a filer who can be claimed as a dependent on another return is outside the tax family (26 CFR 1.36B-1(d)(2)), so the credit uses the tax family size for the poverty line and the coverage family's benchmark premium, on a joint return the other spouse keeps the credit, and the Washington, Massachusetts, Connecticut, Colorado, Maryland, California, New Mexico, Vermont and New Jersey premium programs follow their own rules for that person. + +Preserve supplied benchmark premiums, NY dependent predicates and Medicaid MAGI; rank only enrolled children; test every insured member for shared-policy CSR eligibility; infer both minor joint filers; and allocate shared premiums by coverage month, with zero allocation for empty enrollment. diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml index 7d80d1157d5..beb0e2c7a8a 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml @@ -337,3 +337,221 @@ state_code: TX output: aca_ptc_coverage_family_premium: 0 + +- name: r3 NY changing monthly family tiers allocate each month's silver premium + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + monthly_age: + 2025-01: 30 + 2025-02: 30 + 2025-03: 30 + 2025-04: 30 + 2025-05: 30 + 2025-06: 30 + 2025-07: 30 + 2025-08: 30 + 2025-09: 30 + 2025-10: 30 + 2025-11: 30 + 2025-12: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + spouse: + age: 30 + monthly_age: + 2025-01: 30 + 2025-02: 30 + 2025-03: 30 + 2025-04: 30 + 2025-05: 30 + 2025-06: 30 + 2025-07: 30 + 2025-08: 30 + 2025-09: 30 + 2025-10: 30 + 2025-11: 30 + 2025-12: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + dependent: + age: 29 + monthly_age: + 2025-01: 29 + 2025-02: 29 + 2025-03: 29 + 2025-04: 29 + 2025-05: 29 + 2025-06: 29 + 2025-07: 30 + 2025-08: 30 + 2025-09: 30 + 2025-10: 30 + 2025-11: 30 + 2025-12: 30 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + tax_units: + tax_unit: + members: [head, spouse, dependent] + filing_status: JOINT + aca_magi: 40_000 + selected_marketplace_plan_benchmark_ratio: 0.5 + households: + household: + members: [head, spouse, dependent] + state_code: NY + slcsp_age_0: 6000 + slcsp_rating_area: 3 + output: + aca_ptc_slcsp: + 2025-01: 892.5 + 2025-07: 1000 + selected_marketplace_plan_premium_proxy: 8988.75 + # 26 CFR 1.36B-3(a), (h): sum the monthly allocated premiums: + # 6 * 748.125 * 892.5 / (892.5 + 500) + 6 * 750 * 1000 / (1000 + 500). + aca_ptc_coverage_family_premium: 5876.99 + +- name: r3 monthly bronze allocation preserves a supplied annual selected premium + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + monthly_age: + 2025-01: 30 + 2025-02: 30 + 2025-03: 30 + 2025-04: 30 + 2025-05: 30 + 2025-06: 30 + 2025-07: 30 + 2025-08: 30 + 2025-09: 30 + 2025-10: 30 + 2025-11: 30 + 2025-12: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + spouse: + age: 30 + monthly_age: + 2025-01: 30 + 2025-02: 30 + 2025-03: 30 + 2025-04: 30 + 2025-05: 30 + 2025-06: 30 + 2025-07: 30 + 2025-08: 30 + 2025-09: 30 + 2025-10: 30 + 2025-11: 30 + 2025-12: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + dependent: + age: 29 + monthly_age: + 2025-01: 29 + 2025-02: 29 + 2025-03: 29 + 2025-04: 29 + 2025-05: 29 + 2025-06: 29 + 2025-07: 30 + 2025-08: 30 + 2025-09: 30 + 2025-10: 30 + 2025-11: 30 + 2025-12: 30 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: true + pays_aca_premium: true + tax_units: + tax_unit: + members: [head, spouse, dependent] + filing_status: JOINT + aca_magi: 40_000 + selected_marketplace_plan_category: BRONZE + selected_marketplace_plan_premium_proxy: 12_000 + lcbp: + 2025-01: 1000 + 2025-02: 1000 + 2025-03: 1000 + 2025-04: 1000 + 2025-05: 1000 + 2025-06: 1000 + 2025-07: 500 + 2025-08: 500 + 2025-09: 500 + 2025-10: 500 + 2025-11: 500 + 2025-12: 500 + households: + household: + members: [head, spouse, dependent] + state_code: NY + slcsp_age_0: 6000 + slcsp_rating_area: 3 + output: + # The monthly price pattern allocates 8,000 of the supplied annual total + # to Jan-Jun and 4,000 to Jul-Dec, before applying each month's share. + # 8,000 * 892.5 / 1,392.5 + 4,000 * 1,000 / 1,500. + selected_marketplace_plan_premium_proxy: 12_000 + aca_ptc_coverage_family_premium: 7794.14 + +- name: r3 no Marketplace enrollees receive none of a supplied annual premium + period: 2025 + input: + people: + head: + age: 30 + is_tax_unit_head: true + is_tax_unit_spouse: false + claimable_as_dependent_on_another_return: true + is_aca_eshi_eligible: true + is_aca_marketplace_enrollee: false + pays_aca_premium: false + spouse: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + is_aca_eshi_eligible: true + is_aca_marketplace_enrollee: false + pays_aca_premium: false + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + aca_magi: 22_590 + slcsp: 0 + selected_marketplace_plan_premium_proxy: 7000 + households: + household: + members: [head, spouse] + state_code: TX + output: + aca_ptc_coverage_family_premium: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml index c5a2aa7a740..5f60d687f8b 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml @@ -47,6 +47,8 @@ period: 2026 input: pays_aca_premium: true + # Observed premium payment and coverage are separate inputs. + is_aca_marketplace_enrollee: true selected_marketplace_plan_category: BRONZE slcsp: 10_000 lcbp: 5_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml index 8abcd810034..8facc100839 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml @@ -4,6 +4,7 @@ input: is_aca_ptc_eligible: true pays_aca_premium: true + is_aca_marketplace_enrollee: true slcsp: 1_200 aca_magi: 25_000 aca_required_contribution_percentage: 0.04 @@ -19,6 +20,7 @@ input: is_aca_ptc_eligible: true pays_aca_premium: true + is_aca_marketplace_enrollee: true slcsp: 10_000 aca_magi: 20_000 aca_required_contribution_percentage: 0.04 @@ -63,6 +65,7 @@ input: is_aca_ptc_eligible: true pays_aca_premium: true + is_aca_marketplace_enrollee: true selected_marketplace_plan_category: BRONZE slcsp: 10_000 lcbp: 7_000 diff --git a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py index d9c00f4f054..abee8d41237 100644 --- a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py @@ -20,7 +20,10 @@ class aca_ptc_coverage_family_premium(Variable): "benchmark (aca_ptc_slcsp) over that plus the benchmark of the other " "enrollees, priced as one family of their own. The whole premium is " "the coverage family's when every enrollee is in it, and none is when " - "no enrollee is. It applies to bronze plans too." + "no enrollee is. Each month's share is applied to that month's " + "premium before summing, including for bronze plans. A supplied annual " + "selected premium is distributed in proportion to the monthly " + "selected-plan premium proxies, or evenly if those are unavailable." ) def formula(tax_unit, period, parameters): @@ -28,17 +31,39 @@ def formula(tax_unit, period, parameters): person = tax_unit.members enrolled = person("is_aca_marketplace_enrollee", period) member = person("is_aca_tax_family_member", period) + any_enrollee_covered = tax_unit.any(enrolled & member) every_enrollee_covered = ~tax_unit.any(enrolled & ~member) - coverage_family_benchmark = add(tax_unit, period, ["aca_ptc_slcsp"]) - other_benchmark = sum( - benchmark_premium(tax_unit, month, parameters, ~member, enrolled) - for month in period.get_subperiods(MONTH) + months = period.get_subperiods(MONTH) + monthly_premiums = [ + tax_unit("monthly_selected_marketplace_plan_premium_proxy", month) + for month in months + ] + annual_proxy = sum(monthly_premiums) + allocated_share = 0 + for month, monthly_premium in zip(months, monthly_premiums): + # Preserve a supplied annual premium while retaining the monthly + # price pattern. With derived inputs this is the month's actual + # selected premium divided by the annual selected premium. + month_weight = np.divide( + monthly_premium, + annual_proxy, + out=np.full_like(annual_proxy, 1 / len(months)), + where=annual_proxy > 0, + ) + coverage_family_benchmark = tax_unit("aca_ptc_slcsp", month) + other_benchmark = benchmark_premium( + tax_unit, month, parameters, ~member, enrolled + ) + total = coverage_family_benchmark + other_benchmark + share = np.divide( + coverage_family_benchmark, + total, + out=np.zeros_like(total), + where=total > 0, + ) + allocated_share += month_weight * share + return where( + any_enrollee_covered, + premium * where(every_enrollee_covered, 1, allocated_share), + 0, ) - total = coverage_family_benchmark + other_benchmark - share = np.divide( - coverage_family_benchmark, - total, - out=np.zeros_like(total), - where=total > 0, - ) - return premium * where(every_enrollee_covered, 1, share) diff --git a/policyengine_us/variables/gov/aca/ptc/monthly_selected_marketplace_plan_premium_proxy.py b/policyengine_us/variables/gov/aca/ptc/monthly_selected_marketplace_plan_premium_proxy.py new file mode 100644 index 00000000000..b10b3eefdfc --- /dev/null +++ b/policyengine_us/variables/gov/aca/ptc/monthly_selected_marketplace_plan_premium_proxy.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class monthly_selected_marketplace_plan_premium_proxy(Variable): + value_type = float + entity = TaxUnit + label = "Monthly selected Marketplace plan premium proxy" + unit = USD + definition_period = MONTH + documentation = ( + "The selected plan premium for one month, using that month's bronze " + "or silver price. Plan selection, take-up and enrollment remain annual " + "inputs. The annual selected_marketplace_plan_premium_proxy sums these " + "amounts and continues to accept supplied annual premiums." + ) + + def formula(tax_unit, period, parameters): + year = period.this_year + takes_up_aca_if_eligible = tax_unit("takes_up_aca_if_eligible", year) + person = tax_unit.members + pays_marketplace_premium = tax_unit.any(person("pays_aca_premium", year)) + category = tax_unit("selected_marketplace_plan_category", year) + silver_premium = tax_unit("slcsp", period) * tax_unit( + "selected_marketplace_plan_benchmark_ratio", year + ) + selected_plan_premium = where( + category == category.possible_values.BRONZE, + tax_unit("lcbp", period), + silver_premium, + ) + return where( + takes_up_aca_if_eligible & pays_marketplace_premium, + selected_plan_premium, + 0, + ) diff --git a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py index d25f111c2a5..c7026fa6d65 100644 --- a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py +++ b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py @@ -7,22 +7,4 @@ class selected_marketplace_plan_premium_proxy(Variable): label = "Selected marketplace plan premium proxy" unit = USD definition_period = YEAR - - def formula(tax_unit, period, parameters): - takes_up_aca_if_eligible = tax_unit("takes_up_aca_if_eligible", period) - person = tax_unit.members - pays_marketplace_premium = tax_unit.sum(person("pays_aca_premium", period)) > 0 - category = tax_unit("selected_marketplace_plan_category", period) - silver_premium = tax_unit("slcsp", period) * tax_unit( - "selected_marketplace_plan_benchmark_ratio", period - ) - selected_plan_premium = where( - category == category.possible_values.BRONZE, - tax_unit("lcbp", period), - silver_premium, - ) - return where( - takes_up_aca_if_eligible & pays_marketplace_premium, - selected_plan_premium, - 0, - ) + adds = ["monthly_selected_marketplace_plan_premium_proxy"] From 90b0f12a3247c9a07db14b02b96529c4ee973d2e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 01:11:52 -0400 Subject: [PATCH 30/32] Count enrolled nonpayers in Washington Cascade Care Savings --- changelog.d/claimable-filer-ptc.fixed.md | 2 + .../wa_cascade_care_savings.yaml | 55 +++++++++++++++++++ .../wa_cascade_care_savings_eligible.py | 6 +- .../wa_cascade_care_savings_member_amount.py | 4 +- ...wa_cascade_care_savings_member_eligible.py | 7 ++- 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/changelog.d/claimable-filer-ptc.fixed.md b/changelog.d/claimable-filer-ptc.fixed.md index b65107abedf..7f797d8a743 100644 --- a/changelog.d/claimable-filer-ptc.fixed.md +++ b/changelog.d/claimable-filer-ptc.fixed.md @@ -1,3 +1,5 @@ Apply IRC 36B(c)(1)(D) to the premium tax credit: a filer who can be claimed as a dependent on another return is outside the tax family (26 CFR 1.36B-1(d)(2)), so the credit uses the tax family size for the poverty line and the coverage family's benchmark premium, on a joint return the other spouse keeps the credit, and the Washington, Massachusetts, Connecticut, Colorado, Maryland, California, New Mexico, Vermont and New Jersey premium programs follow their own rules for that person. Preserve supplied benchmark premiums, NY dependent predicates and Medicaid MAGI; rank only enrolled children; test every insured member for shared-policy CSR eligibility; infer both minor joint filers; and allocate shared premiums by coverage month, with zero allocation for empty enrollment. + +Count enrolled nonpayers in Washington Cascade Care Savings Group 2 before applying the household premium cap. diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml index 062ef7cd7cd..58f689b4f15 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml @@ -912,3 +912,58 @@ output: # Both spouses are Group 2: 2 * 12 * 250 = 6,000, below the cap of 12,000. wa_cascade_care_savings: 6000 + +- name: Review r4 WA Group 2 includes an enrolled spouse with no marginal premium + period: 2026 + absolute_error_margin: 0.02 + input: + people: + head: + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + spouse: + age: 19 + is_tax_unit_head: false + is_tax_unit_spouse: true + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_aca_marketplace_enrollee: true + child1: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + child2: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + child3: + age: 20 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_aca_marketplace_enrollee: true + tax_units: + tax_unit: + members: [head, spouse, child1, child2, child3] + filing_status: JOINT + tax_unit_is_filer: true + aca_magi: 80_000 + households: + household: + members: [head, spouse, child1, child2, child3] + state_code: WA + slcsp_age_0: 6_000 + output: + pays_aca_premium: [true, false, true, true, true] + is_aca_tax_family_member: [true, false, true, true, true] + wa_cascade_care_savings_member_eligible: [true, true, true, true, true] + wa_cascade_care_savings_member_amount: [660, 3_000, 660, 660, 660] + # Four Group 1 enrollees plus the Group 2 spouse: 4 * 660 + 3,000. + # The household residual premium cap of 6,693.12 does not bind. + wa_cascade_care_savings: 5_640 diff --git a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_eligible.py b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_eligible.py index 55b89a4ad29..3e15ac1fef3 100644 --- a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_eligible.py +++ b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_eligible.py @@ -16,14 +16,16 @@ class wa_cascade_care_savings_eligible(Variable): "in effect, household income (ACA MAGI per 26 U.S.C. 36B(d)(2)) is at " "or below 250% of the federal poverty line, and at least one member is " "a Cascade Care Savings enrollee (Group 1, eligible for the federal " - "ACA premium tax credit; or Group 3, undocumented and lacking minimum " + "ACA premium tax credit; Group 2, enrolled outside the federal tax " + "family; or Group 3, undocumented and lacking minimum " "essential coverage through Washington Apple Health Expansion or Apple " "Health for Kids). Section 4(1)(c) imposes only the 250% FPL upper " "bound; it sets no income floor. Washington residency is enforced " "by defined_for. Not modeled: the Cascade Care standard-plan enrollment " "requirement (otherwise-eligible Marketplace enrollees are treated as " "Cascade-enrolled), the filing-behavior exclusions of Section 4(2), " - "the COFA Islander Premium Assistance ineligibility test, and Group 2." + "the COFA Islander Premium Assistance ineligibility test, and other " + "Group 2 members not identifiable in the model." ) def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py index de701b06234..dbd2f886da3 100644 --- a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py +++ b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py @@ -60,7 +60,9 @@ def formula(person, period, parameters): group_3 = undocumented & ~apple_health_expansion & ~apple_health_kids & ~group_1 # Group 2: QHP-eligible enrollees outside the federal tax family, who # have no federal subsidy (see wa_cascade_care_savings_member_eligible). - group_2 = person("pays_aca_premium", period) & ~person( + # Count eligible enrollees before the household cap, including a + # covered child who adds no marginal premium (Policy Section 5(1)(c)). + group_2 = person("is_aca_marketplace_enrollee", period) & ~person( "is_aca_tax_family_member", period ) with_subsidy = where(group_1, p.amount.with_federal_subsidy, 0) diff --git a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py index d931c332c49..ac12d3f867c 100644 --- a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py +++ b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py @@ -52,9 +52,10 @@ def formula(person, period, parameters): # PMPM methodology defines Group 2 as QHP-eligible residents "not # eligible for APTC Federal subsidies but eligible for CCS", and # Policy Section 4(2) does not list being claimable among its - # exclusions. pays_aca_premium already excludes undocumented people, - # so Groups 2 and 3 do not overlap. - group_2 = person("pays_aca_premium", period) & ~person( + # exclusions. Enrollment includes covered children who add no marginal + # premium, and excludes undocumented people, so Groups 2 and 3 do not + # overlap. Count enrollees before the cap (Policy Section 5(1)(c)-(d)). + group_2 = person("is_aca_marketplace_enrollee", period) & ~person( "is_aca_tax_family_member", period ) return group_1 | group_2 | group_3 From 1c1450ee4ee785f9073cb06350fdb8bc4589fb2b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 01:14:22 -0400 Subject: [PATCH 31/32] Complete partially inferred joint ACA filers --- changelog.d/claimable-filer-ptc.fixed.md | 2 + .../gov/aca/eligibility/aca_magi.yaml | 29 +++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 172 ++++++++++++++++++ .../variables/gov/aca/eligibility/aca_magi.py | 7 +- .../gov/aca/eligibility/is_aca_filer.py | 35 ++-- .../eligibility/is_aca_tax_family_member.py | 23 +-- 6 files changed, 230 insertions(+), 38 deletions(-) diff --git a/changelog.d/claimable-filer-ptc.fixed.md b/changelog.d/claimable-filer-ptc.fixed.md index 7f797d8a743..c2f9c78d645 100644 --- a/changelog.d/claimable-filer-ptc.fixed.md +++ b/changelog.d/claimable-filer-ptc.fixed.md @@ -3,3 +3,5 @@ Apply IRC 36B(c)(1)(D) to the premium tax credit: a filer who can be claimed as Preserve supplied benchmark premiums, NY dependent predicates and Medicaid MAGI; rank only enrolled children; test every insured member for shared-policy CSR eligibility; infer both minor joint filers; and allocate shared premiums by coverage month, with zero allocation for empty enrollment. Count enrolled nonpayers in Washington Cascade Care Savings Group 2 before applying the household premium cap. + +Complete partially inferred joint ACA filers without an adult age cutoff, and use the completed set for tax-family membership and MAGI additions. diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml index 5be0b7aa513..4eba57084e5 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml @@ -383,3 +383,32 @@ # Both spouses' additions remain; only the excluded child's 4,000 leaves. medicaid_magi: 29_000 aca_magi: 25_000 + +- name: Review r4 partially inferred joint filers both retain their MAGI additions + period: 2025 + input: + people: + older_spouse: + age: 18 + claimable_as_dependent_on_another_return: true + tax_exempt_interest_income: 2_000 + younger_spouse: + age: 17 + tax_exempt_interest_income: 3_000 + child: + age: 1 + tax_exempt_interest_income: 4_000 + tax_units: + tax_unit: + members: [older_spouse, younger_spouse, child] + filing_status: JOINT + adjusted_gross_income: 20_000 + households: + household: + members: [older_spouse, younger_spouse, child] + state_code: TX + output: + # Completing the joint filers keeps the minor spouse's additions while + # the excluded child's additions leave ACA household income. + medicaid_magi: 29_000 + aca_magi: 25_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 7f5ca3d972d..fecac1c3eb0 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -1083,3 +1083,175 @@ output: # 22,590 / 20,440 = 110% FPL: 0% contribution; 2 * 6,000. aca_ptc: 12_000 + +- name: Review r4 joint filers ages 18 and 17 with claimable age 18, inferred roles + period: 2025 + absolute_error_margin: 0.01 + input: + people: + older_spouse: + age: 18 + claimable_as_dependent_on_another_return: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + younger_spouse: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [older_spouse, younger_spouse] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [older_spouse, younger_spouse] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2) includes both joint filers regardless of age; + # the spouse claimable on another return is outside this tax family. + is_aca_tax_family_member: [false, true] + aca_ptc: 6000 + is_aca_filer: [true, true] + +- name: Review r4 joint filers ages 18 and 17 with claimable age 18, explicit-role control + period: 2025 + absolute_error_margin: 0.01 + input: + people: + older_spouse: + age: 18 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + younger_spouse: + age: 17 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [older_spouse, younger_spouse] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [older_spouse, younger_spouse] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2) includes both joint filers regardless of age; + # the spouse claimable on another return is outside this tax family. + is_aca_tax_family_member: [false, true] + aca_ptc: 6000 + is_aca_filer: [true, true] + +- name: Review r4 joint filers ages 18 and 17 with claimable age 17, inferred roles + period: 2025 + absolute_error_margin: 0.01 + input: + people: + older_spouse: + age: 18 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + younger_spouse: + age: 17 + claimable_as_dependent_on_another_return: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [older_spouse, younger_spouse] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [older_spouse, younger_spouse] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2) includes both joint filers regardless of age; + # the spouse claimable on another return is outside this tax family. + is_aca_tax_family_member: [true, false] + aca_ptc: 6000 + is_aca_filer: [true, true] + +- name: Review r4 joint filers ages 18 and 17 with claimable age 17, explicit-role control + period: 2025 + absolute_error_margin: 0.01 + input: + people: + older_spouse: + age: 18 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + younger_spouse: + age: 17 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_aca_marketplace_enrollee: true + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [older_spouse, younger_spouse] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [older_spouse, younger_spouse] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2) includes both joint filers regardless of age; + # the spouse claimable on another return is outside this tax family. + is_aca_tax_family_member: [true, false] + aca_ptc: 6000 + is_aca_filer: [true, true] diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py index 61bb6dff35d..0c19bc3a8c8 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py @@ -18,9 +18,10 @@ class aca_magi(Variable): "medicaid_magi's AGI plus MAGI additions, without the tax-exempt " "interest and Social Security of the return's dependents who are " "outside the tax family (is_aca_tax_family_member), floored at zero " - "once. Those amounts are left out only on a return whose head or " - "spouse can be claimed elsewhere (or, in a tax unit with neither, " - "whose inferred filer can be); otherwise this equals medicaid_magi. " + "once. Those amounts are left out only on a return whose ACA " + "filer can be claimed elsewhere, including a filer inferred to " + "complete absent or partial head/spouse roles; otherwise this equals " + "medicaid_magi. " "Tax unit AGI already leaves dependents' income out." ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py index 27a2974c4b1..44ee89b1974 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py @@ -8,28 +8,27 @@ class is_aca_filer(Variable): definition_period = YEAR reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d" documentation = ( - "Uses the tax unit's head and spouse when either role exists. When " - "adult role inference leaves both absent, the oldest member files, " - "or the two oldest on a joint return. Supplied tax-unit dependency " - "hints exclude dependents from this fallback when possible. Ages " - "alone cannot distinguish a filer from an equally old dependent; " - "supply explicit head/spouse roles for such ambiguous households. " - "This ACA-only inference does not change the tax unit's other roles." + "Keeps the tax unit's head and spouse, completing an absent or " + "partial set with the oldest remaining members: one filer, or two " + "on a joint return. Supplied tax-unit dependency hints exclude " + "dependents from this fallback when possible. Ages alone cannot " + "distinguish a filer from an equally old dependent; supply explicit " + "head/spouse roles for such ambiguous households. This ACA-only " + "inference has no adult-age cutoff and does not change the tax " + "unit's other roles." ) def formula(person, period, parameters): tax_unit = person.tax_unit filer = person("is_tax_unit_head_or_spouse", period) - has_filer = tax_unit.any(filer) - # Read the stored hint without deriving dependency from the roles we - # are filling in. If every member is marked dependent (including a - # cached role-derived array for minors), retain the age fallback. - dependent = person.simulation.get_array("is_tax_unit_dependent", period) - candidate = person.filled_array(True) - if dependent is not None: - candidate = where(tax_unit.any(~dependent), ~dependent, candidate) - rank = person.get_rank(tax_unit, -person("age", period), candidate) status = tax_unit("filing_status", period) joint = status == status.possible_values.JOINT - inferred = candidate & (rank < (1 + joint)) - return where(has_filer, filer, inferred) + missing_filers = max_(0, 1 + joint - tax_unit.sum(filer)) + candidate = ~filer + # A calculated dependent array reflects the incomplete roles we are + # filling in, so only supplied dependency hints can restrict candidates. + if has_input_for_period(person.simulation, "is_tax_unit_dependent", period): + dependent = person("is_tax_unit_dependent", period) + candidate &= where(tax_unit.any(~dependent), ~dependent, True) + rank = person.get_rank(tax_unit, -person("age", period), candidate) + return filer | (candidate & (rank < missing_filers)) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py index 8394d236604..d4ae8027480 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -28,10 +28,10 @@ class is_aca_tax_family_member(Variable): "Dependent Taxpayer Test). No credit is allowed to someone another " "taxpayer can claim (IRC 36B(c)(1)(D)), so a return whose every head " "and spouse can be claimed has no applicable taxpayer and no family " - "for the credit. A tax unit with neither a head nor a spouse (role " - "inference makes the oldest adult the head, so a unit of minors has " - "none) uses is_aca_filer to identify one filer, or two on a joint " - "return, using age and supplied dependency hints. Explicit roles are " + "for the credit. is_aca_filer completes an absent or partial set " + "of head/spouse roles to identify one filer, or two on a joint " + "return, using age and supplied dependency hints without an adult-age " + "cutoff. Explicit roles are " "needed when ages cannot distinguish filers from dependents. A " "dependent on a return with an applicable taxpayer is a member even if " "another taxpayer could also claim them: this return claims them." @@ -40,19 +40,8 @@ class is_aca_tax_family_member(Variable): def formula(person, period, parameters): filer = person("is_aca_filer", period) claimable = person("claimable_as_dependent_on_another_return", period) - has_roles = person.tax_unit.any(person("is_tax_unit_head_or_spouse", period)) - applicable_taxpayer = where( - has_roles, - person.tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0, - person.tax_unit.any(filer & ~claimable), - ) + applicable_taxpayer = person.tax_unit.any(filer & ~claimable) exception = person("dependent_claimant_filing_exception", period) - claims_dependents = ~where( - has_roles, - person.tax_unit( - "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period - ), - person.tax_unit.any(filer & claimable & ~exception), - ) + claims_dependents = ~person.tax_unit.any(filer & claimable & ~exception) dependent_member = applicable_taxpayer & claims_dependents return where(filer, ~claimable, dependent_member) From 09544440a74f18410a67a8d13a826746bf0372ea Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 01:15:27 -0400 Subject: [PATCH 32/32] Preserve supplied child predicates for nonpaying enrollees --- changelog.d/claimable-filer-ptc.fixed.md | 2 + .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 154 ++++++++++++++++++ .../is_aca_family_tier_dependent_child.py | 12 +- 3 files changed, 164 insertions(+), 4 deletions(-) diff --git a/changelog.d/claimable-filer-ptc.fixed.md b/changelog.d/claimable-filer-ptc.fixed.md index c2f9c78d645..851b57c604f 100644 --- a/changelog.d/claimable-filer-ptc.fixed.md +++ b/changelog.d/claimable-filer-ptc.fixed.md @@ -5,3 +5,5 @@ Preserve supplied benchmark premiums, NY dependent predicates and Medicaid MAGI; Count enrolled nonpayers in Washington Cascade Care Savings Group 2 before applying the household premium cap. Complete partially inferred joint ACA filers without an adult age cutoff, and use the completed set for tax-family membership and MAGI additions. + +Honor supplied ACA family-tier child predicates for nonpayers in their supplied month while retaining uncapped calculated classifications in other months. diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml index 047d188ecb4..60aa1b8893c 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -669,3 +669,157 @@ is_aca_marketplace_enrollee: [true, true, true, true, true] slcsp: 4713.8 aca_ptc_slcsp: 4483.7 +- name: r4 NY supplied family-tier child classification applies to an enrolled nonpayer + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + monthly_age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: false + spouse: + age: 40 + monthly_age: 40 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + is_aca_marketplace_enrollee: {2025: true} + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_aca_family_tier_dependent_child: false + oldest_child: + age: 20 + monthly_age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: true + second_child: + age: 19 + monthly_age: 19 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: true + third_child: + age: 18 + monthly_age: 18 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: true + youngest_child: + age: 10 + monthly_age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: false + tax_units: + tax_unit: + members: [head, spouse, oldest_child, second_child, third_child, youngest_child] + filing_status: JOINT + households: + household: + members: [head, spouse, oldest_child, second_child, third_child, youngest_child] + state_code: NY + state_code_str: NY + slcsp_age_0: 500 + output: + # The fourth enrolled child pays no marginal age-rated premium. The + # supplied false classification still counts that enrollee as an adult + # when pricing the independent head's coverage family: 500 * 2.85. + pays_aca_premium: [true, true, true, true, true, false] + is_aca_tax_family_member: [true, false, true, true, true, true] + aca_ptc_slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_multiplier: 2.85 + aca_ptc_slcsp: 1425 + +- name: r4 NY supplied family-tier child classification applies only in its supplied month + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + monthly_age: {2025-01: 40, 2025-02: 40} + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: {2025-01: false} + spouse: + age: 40 + monthly_age: {2025-01: 40, 2025-02: 40} + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + is_aca_marketplace_enrollee: {2025: true} + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_aca_family_tier_dependent_child: {2025-01: false} + oldest_child: + age: 20 + monthly_age: {2025-01: 20, 2025-02: 20} + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: {2025-01: true} + second_child: + age: 19 + monthly_age: {2025-01: 19, 2025-02: 19} + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: {2025-01: true} + third_child: + age: 18 + monthly_age: {2025-01: 18, 2025-02: 18} + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: {2025-01: true} + youngest_child: + age: 10 + monthly_age: {2025-01: 10, 2025-02: 10} + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_aca_marketplace_enrollee: {2025: true} + is_aca_family_tier_dependent_child: {2025-01: false} + tax_units: + tax_unit: + members: [head, spouse, oldest_child, second_child, third_child, youngest_child] + filing_status: JOINT + households: + household: + members: [head, spouse, oldest_child, second_child, third_child, youngest_child] + state_code: NY + state_code_str: NY + slcsp_age_0: {2025-01: 500, 2025-02: 500} + output: + pays_aca_premium: [true, true, true, true, true, false] + is_aca_tax_family_member: [true, false, true, true, true, true] + # January honors the supplied false predicate. In February the public + # predicate is calculated, so the nonpayer uses the raw child condition. + aca_ptc_slcsp_family_tier_category: + 2025-01: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + 2025-02: ONE_ADULT_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_multiplier: + 2025-01: 2.85 + 2025-02: 1.7 + aca_ptc_slcsp: + 2025-01: 1425 + 2025-02: 850 diff --git a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py index c23adfbe551..67913550225 100644 --- a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py +++ b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py @@ -18,13 +18,17 @@ def family_tier_child_condition(person, period, parameters): def family_tier_child_condition_for_enrollee(person, period, parameters): """Preserve supplied child classifications when repricing enrollees. - The public predicate includes premium payment. Use its supplied or - calculated value for payers, and apply the uncapped child condition to - enrolled children who add no age-rated premium to the whole policy. + The public predicate includes premium payment. A supplied predicate + takes precedence for every enrollee in that month, including nonpayers. + Otherwise use the calculated predicate for payers and the uncapped child + condition for enrolled children who add no age-rated premium to the policy. """ + name = "is_aca_family_tier_dependent_child" + if has_input_for_period(person.simulation, name, period): + return person(name, period) return where( person("pays_aca_premium", period.this_year), - person("is_aca_family_tier_dependent_child", period), + person(name, period), family_tier_child_condition(person, period, parameters), )