diff --git a/changelog.d/claimable-filer-ptc.fixed.md b/changelog.d/claimable-filer-ptc.fixed.md new file mode 100644 index 00000000000..851b57c604f --- /dev/null +++ b/changelog.d/claimable-filer-ptc.fixed.md @@ -0,0 +1,9 @@ +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. + +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/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/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py new file mode 100644 index 00000000000..d65b01c4cdb --- /dev/null +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -0,0 +1,459 @@ +"""The premium tax credit for a filer who can be claimed as a dependent. + +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 in every year 2021-2026 (spouses claimable, +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` 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 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 + 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 +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, + money, +) + +# 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", + "aca_magi_fraction", + "slcsp", + "aca_ptc_slcsp", + "aca_ptc", + "aca_ptc_coverage_family_premium", + "used_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 +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 +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(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)), + } + + +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 _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 _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) + + +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("is_aca_marketplace_enrollee", year), dtype=float + ) + 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 = ( + _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(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)) + 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" + aca_magi = _calc(sim, "aca_magi", year) + medicaid_magi = _calc(sim, "medicaid_magi", year) + 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) + 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("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() + 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 + ) + 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_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_claimable] > 0).any() + if year >= 2026: + wraps = sum(_calc(sim, name, year) for name in STATE_WRAPS) + 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(): + 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, 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) + 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) + for more, fewer, label in ( + (1, 0, "first adult"), + (2, 0, "second adult"), + (3, 1, "both after first"), + (3, 2, "both after second"), + ): + 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 = removed[:, more] > removed[:, 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']} {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=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_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 = ( + _with_dependent_income(_seeded_couples(STATES, per_state=2)) + + _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. + 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/csr/marketplace_csr_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml index b3780f6fe83..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 @@ -28,3 +28,263 @@ 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 + +- 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/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/eligibility/aca_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml new file mode 100644 index 00000000000..ea84ca29d74 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml @@ -0,0 +1,280 @@ +- 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 + 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_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 + 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 + 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 + claimable_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 + 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 + 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 + 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 + 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_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 + 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 + households: + household: + members: [person_b, person_a] + 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..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 @@ -41,3 +41,374 @@ # 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 + +- 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 + +- 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 + +- 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/eligibility/aca_magi_fraction.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml new file mode 100644 index 00000000000..5f8e0b0d996 --- /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 + 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_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 + 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: 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 + claimable_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..8e6b856b402 --- /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 + claimable_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 + claimable_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..07a0ecbaf5e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml @@ -0,0 +1,325 @@ +- 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 + claimable_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 + 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_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 + 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 + 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 + 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 + 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 + 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 + +- 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_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..cd17edc030b --- /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 + 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_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 + 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 + 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_marketplace_enrollee.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml new file mode 100644 index 00000000000..233ab78b499 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml @@ -0,0 +1,65 @@ +- 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 + # Above the six-person poverty line so the coverage gap does not bind. + aca_magi: 50_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_ptc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml index 0c8789a0467..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 @@ -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 + 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_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 + 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: 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 + claimable_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 + 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 + 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 + 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 + 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_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 + 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 + 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 + 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_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 + 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: 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..0b306fec450 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -0,0 +1,669 @@ +- 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 + claimable_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 + 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_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 + 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 + 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 + 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 + 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 + 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 another taxpayer could also claim 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 + claimable_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] + +- 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] + +- 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] + +# 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/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 2214672a9e5..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 @@ -276,3 +276,982 @@ 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 + claimable_as_dependent_on_another_return: true + slcsp_age_curve_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + 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 + claimable_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 + 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 + 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_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 + claimable_as_dependent_on_another_return: true + slcsp_age_curve_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + 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 + claimable_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 + 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 + 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_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 + claimable_as_dependent_on_another_return: true + slcsp_age_curve_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + +- 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_multiplier: 1 + 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_multiplier: 1 + 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 + # Annual base: adults cost 6,000 and children cost 3,000. + slcsp_age_0: 6000 + 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 + +- 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_multiplier: 1 + 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_multiplier: 1 + 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_multiplier: 0.5 + 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 + # 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 + 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_multiplier: 1 + 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_multiplier: 1 + 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_multiplier: 0.5 + 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 + # 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 + 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_multiplier: 1 + 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_multiplier: 1 + 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_multiplier: 0.5 + 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 + # 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 + 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_multiplier: 1 + 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_multiplier: 1 + 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_multiplier: 0.5 + 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 + # 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 + 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 + +- 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 + +- 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/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..beb0e2c7a8a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml @@ -0,0 +1,557 @@ +- 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 + +- 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 + +- 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 ddf141f5162..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 @@ -55,3 +57,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..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 @@ -74,3 +77,86 @@ 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 new file mode 100644 index 00000000000..60aa1b8893c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -0,0 +1,825 @@ +- 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 + 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 + 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 + claimable_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 + claimable_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 + 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 + 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_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 + is_aca_marketplace_enrollee: {2025: 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 + 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] + 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_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 + child: + 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] + 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 + is_aca_marketplace_enrollee: {2025: 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 + 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] + 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_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 + child: + 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] + 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 + +- 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: 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 + 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 +- 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/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..2e7b08c27a7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml @@ -0,0 +1,309 @@ +- 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 + is_aca_marketplace_enrollee: {2025: 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 + 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] + 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_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 + child: + 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] + 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 + is_aca_marketplace_enrollee: {2025: 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 + is_aca_marketplace_enrollee: {2025: true} + claimable_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 + is_aca_marketplace_enrollee: {2025: 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_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] + 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 + is_aca_marketplace_enrollee: {2025: 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 + 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] + 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_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 + child: + 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] + 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 + is_aca_marketplace_enrollee: {2025: 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 + is_aca_marketplace_enrollee: {2025: true} + claimable_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 + is_aca_marketplace_enrollee: {2025: 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_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] + 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..d69e4eb7e50 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml @@ -0,0 +1,233 @@ +- 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 + is_aca_marketplace_enrollee: {2025: 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 + 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] + 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_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 + child: + 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] + 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 + is_aca_marketplace_enrollee: {2025: 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 + is_aca_marketplace_enrollee: {2025: true} + claimable_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 + is_aca_marketplace_enrollee: {2025: 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 + is_aca_marketplace_enrollee: {2025: 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 + 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] + 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_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 + child: + 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] + 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 + is_aca_marketplace_enrollee: {2025: 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 + is_aca_marketplace_enrollee: {2025: true} + claimable_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 + is_aca_marketplace_enrollee: {2025: 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/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/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..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 @@ -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 + 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: 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 + 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: 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..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 @@ -681,3 +681,252 @@ 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 + 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: 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 + 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: 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 + 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 + 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: 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 + +- 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + # 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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/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..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 @@ -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 + 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: 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 + 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: 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 + 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 + 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: 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..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 @@ -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 + 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: 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 + 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: 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 + 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 + 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: 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..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 @@ -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 + 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: 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 + 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: 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 dc03a0ca2b0..fd2aeaf08af 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 @@ -813,3 +813,165 @@ state_code: MD output: md_premium_assistance: 89.5 + +- 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 + 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: 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 + 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: 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 + 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 + 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: 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..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 @@ -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 + 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: 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 + 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: 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 + 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 + 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: 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..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 @@ -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 + 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: 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 + 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: 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 9b3e0c164a7..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] @@ -887,3 +893,169 @@ # 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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. + # 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_multiplier: 1 + 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_multiplier: 1 + 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: 23_475 + tax_unit_is_filer: true + households: + 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 + +- 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 + 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 + 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: 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 8f51d63d7df..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] @@ -262,3 +275,130 @@ 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_multiplier: 1 + 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_multiplier: 1 + 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 + # 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 + 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_multiplier: 1 + 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_multiplier: 1 + 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: 70_000 + tax_unit_is_filer: true + households: + 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 + 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 + 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 + 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: 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..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 @@ -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 + 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 + 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 + 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: 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 + 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: 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..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 @@ -749,3 +749,221 @@ 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 + 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: 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 + 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: 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 + 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 + 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: 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 + +- 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/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..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 @@ -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 + 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: 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 + 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: 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..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 @@ -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 + 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: 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 + 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: 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/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml index ba79805f0ff..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 @@ -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 + 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_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 + 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: 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..8c568ee903e 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 + 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_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 + 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: 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..cd3d6932461 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 + 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_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 + 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: 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/csr/marketplace_csr_eligible.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py index 837491802ee..30cdb0235f5 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,36 @@ 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("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 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_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/aca_fpg.py b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py new file mode 100644 index 00000000000..0ba7f9a9adc --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py @@ -0,0 +1,35 @@ +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 " + "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): + 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.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py index 2b9764bf6af..0c19bc3a8c8 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py @@ -6,4 +6,50 @@ 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'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 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." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + 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 + * ( + person("tax_exempt_interest_income", period) + + person("social_security", period) + ) + ) + # 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 + ) + # 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/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..96889d8b9ec --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py @@ -0,0 +1,22 @@ +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 (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 new file mode 100644 index 00000000000..68907d3f4e4 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py @@ -0,0 +1,22 @@ +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" + documentation = ( + "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("is_aca_marketplace_enrollee", period) & person( + "is_aca_tax_family_member", period + ) 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..44ee89b1974 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_filer.py @@ -0,0 +1,34 @@ +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 = ( + "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) + status = tax_unit("filing_status", period) + joint = status == status.possible_values.JOINT + 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_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_ptc_eligible.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py index b21baa83d3c..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 @@ -6,18 +6,12 @@ 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) - 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 + return person("pays_aca_premium", period) & person( + "is_aca_ptc_eligible_before_premium", period ) - is_income_eligible = standard_income_eligible | below_fpl_exception - - return person("pays_aca_premium", period) & ~separate & is_income_eligible 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 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..d4ae8027480 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -0,0 +1,47 @@ +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", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", + ) + documentation = ( + "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. 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." + ) + + def formula(person, period, parameters): + filer = person("is_aca_filer", period) + claimable = person("claimable_as_dependent_on_another_return", period) + applicable_taxpayer = person.tax_unit.any(filer & ~claimable) + exception = person("dependent_claimant_filing_exception", period) + claims_dependents = ~person.tax_unit.any(filer & claimable & ~exception) + dependent_member = applicable_taxpayer & claims_dependents + return where(filer, ~claimable, dependent_member) 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..ee13d6d755e 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 " + "enrolled 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 covered children (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_before_filing_gate.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc_before_filing_gate.py index 9621dde4694..a7753172f2e 100644 --- a/policyengine_us/variables/gov/aca/ptc/aca_ptc_before_filing_gate.py +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc_before_filing_gate.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) return max_(0, plan_cost - income * applicable_figure) 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..abee8d41237 --- /dev/null +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py @@ -0,0 +1,69 @@ +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): + 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 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. 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): + premium = tax_unit("selected_marketplace_plan_premium_proxy", period) + 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) + 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, + ) 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"] 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/aca/slspc/aca_ptc_slcsp.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py new file mode 100644 index 00000000000..94e999c8ab7 --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py @@ -0,0 +1,75 @@ +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 & enrolled & is_child) + charged = group & enrolled & (~is_child | (child_rank < p.max_child_count)) + 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( + 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): + 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 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("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 + ) + 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/is_aca_family_tier_dependent_child.py b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py index 89231d0249b..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 @@ -1,6 +1,38 @@ from policyengine_us.model_api import * +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) + | 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. 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(name, period), + family_tier_child_condition(person, period, parameters), + ) + + class is_aca_family_tier_dependent_child(Variable): value_type = bool entity = Person @@ -8,14 +40,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_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 147aa042035..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,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_for_enrollee, +) class FamilyTierCategory(Enum): @@ -10,6 +13,82 @@ 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 & 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) + + 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 +99,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..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,68 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.is_aca_family_tier_dependent_child import ( + family_tier_child_condition_for_enrollee, +) from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( FamilyTierCategory, ) +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 & 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 & 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 +73,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 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 eb31d0a2ee2..c7868e6eeaa 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("files_return_for_aca_ptc", 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 0395799eb38..c1cde501ba5 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 @@ -47,9 +47,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..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 @@ -49,7 +49,10 @@ 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 (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) 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..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 @@ -13,7 +13,12 @@ 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 (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: " "that variable embeds the federal 400% FPL income cliff (the PY2026 " @@ -45,7 +50,13 @@ 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) + # 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 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 718534d46ea..2228be7afeb 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_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 307e9cc6f90..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 @@ -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,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 + # Group 2: QHP-eligible enrollees outside the federal tax family, who + # have no federal subsidy (see wa_cascade_care_savings_member_eligible). + # 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) - 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..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 @@ -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,14 @@ 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. 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