From bdd90309bfcbbf4ce0b4ac50ad78079be985a3be Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 12:36:16 -0400 Subject: [PATCH 1/7] Keep a filer who can be claimed elsewhere out of the premium tax credit family IRC 36B(c)(1)(D) excludes anyone another taxpayer can claim from being an applicable taxpayer, and 26 CFR 1.36B-1(d)(2) leaves such a spouse out of a joint return's family. Add is_aca_tax_family_member and aca_tax_family_size, measure household income against the poverty line for that family (aca_fpg, aca_magi_fraction), gate is_aca_ptc_eligible and the below-FPL immigration exception on it, and price the credit on the coverage family's benchmark (aca_ptc_slcsp, which equals slcsp whenever every enrollee is in the family). The NY/VT family-tier logic becomes shared functions so the benchmark can apply it to the coverage family. The three ACA reforms that override is_aca_ptc_eligible keep the gate. Co-Authored-By: Claude Opus 5.5 --- changelog.d/claimable-filer-ptc.fixed.md | 1 + .../reforms/aca/aca_ptc_700_fpl_cliff.py | 11 +- .../reforms/aca/aca_ptc_additional_bracket.py | 5 + .../reforms/aca/aca_ptc_simplified_bracket.py | 5 + .../baseline/gov/aca/eligibility/aca_fpg.yaml | 238 +++++++++ .../aca/eligibility/aca_magi_fraction.yaml | 123 +++++ ...a_ptc_below_fpl_immigration_exception.yaml | 105 ++++ .../aca/eligibility/aca_tax_family_size.yaml | 233 +++++++++ .../is_aca_coverage_family_member.yaml | 115 +++++ .../aca/eligibility/is_aca_ptc_eligible.yaml | 344 +++++++++++++ .../eligibility/is_aca_tax_family_member.yaml | 209 ++++++++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 470 ++++++++++++++++++ .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 372 ++++++++++++++ .../aca_ptc_slcsp_family_tier_category.yaml | 285 +++++++++++ .../aca_ptc_slcsp_family_tier_multiplier.yaml | 215 ++++++++ .../policy/contrib/aca/ptc_700_fpl_cliff.yaml | 73 +++ .../contrib/aca/ptc_additional_bracket.yaml | 73 +++ .../contrib/aca/ptc_simplified_bracket.yaml | 73 +++ .../variables/gov/aca/eligibility/aca_fpg.py | 31 ++ .../gov/aca/eligibility/aca_magi_fraction.py | 19 +- ...aca_ptc_below_fpl_immigration_exception.py | 8 +- .../aca/eligibility/aca_tax_family_size.py | 21 + .../is_aca_coverage_family_member.py | 25 + .../aca/eligibility/is_aca_ptc_eligible.py | 16 +- .../eligibility/is_aca_tax_family_member.py | 37 ++ .../variables/gov/aca/ptc/aca_ptc.py | 4 +- .../variables/gov/aca/slspc/aca_ptc_slcsp.py | 42 ++ .../aca_ptc_slcsp_family_tier_category.py | 26 + .../aca_ptc_slcsp_family_tier_multiplier.py | 21 + .../aca/slspc/slcsp_family_tier_category.py | 154 +++--- .../aca/slspc/slcsp_family_tier_multiplier.py | 106 ++-- 31 files changed, 3317 insertions(+), 143 deletions(-) create mode 100644 changelog.d/claimable-filer-ptc.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml create mode 100644 policyengine_us/variables/gov/aca/eligibility/aca_fpg.py create mode 100644 policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py create mode 100644 policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py create mode 100644 policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py create mode 100644 policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py diff --git a/changelog.d/claimable-filer-ptc.fixed.md b/changelog.d/claimable-filer-ptc.fixed.md new file mode 100644 index 00000000000..00c137474dd --- /dev/null +++ b/changelog.d/claimable-filer-ptc.fixed.md @@ -0,0 +1 @@ +Apply IRC 36B(c)(1)(D) to the premium tax credit: a filer who can be claimed as a dependent on another return is outside the tax family (26 CFR 1.36B-1(d)(2)), so the credit uses the tax family size for the poverty line and the coverage family's benchmark premium, on a joint return the other spouse keeps the credit, and the Washington, Massachusetts, Connecticut, Colorado, Maryland, California, New Mexico, Vermont and New Jersey premium programs follow their own rules for that person. diff --git a/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py b/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py index c9d449f6981..38002e777ac 100644 --- a/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py +++ b/policyengine_us/reforms/aca/aca_ptc_700_fpl_cliff.py @@ -66,7 +66,16 @@ def formula(person, period, parameters): ) is_income_eligible = standard_income_eligible | below_fpl_exception - return person("pays_aca_premium", period) & ~separate & is_income_eligible + # The reform leaves 26 U.S.C. 36B(c)(1)(D) in place: someone + # another taxpayer can claim is outside the tax family. + tax_family_member = person("is_aca_tax_family_member", period) + + return ( + person("pays_aca_premium", period) + & tax_family_member + & ~separate + & is_income_eligible + ) class reform(Reform): def apply(self): diff --git a/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py b/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py index a7ce0497a2d..b98cda69863 100644 --- a/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py +++ b/policyengine_us/reforms/aca/aca_ptc_additional_bracket.py @@ -73,11 +73,16 @@ def formula(person, period, parameters): child_pays = person("aca_child_index", period) <= p_aca.max_child_count pays_aca_premium = is_aca_adult | child_pays + # The reform leaves 26 U.S.C. 36B(c)(1)(D) in place: someone + # another taxpayer can claim is outside the tax family. + tax_family_member = person("is_aca_tax_family_member", period) + return ( is_status_eligible & is_coverage_eligible & is_income_eligible & pays_aca_premium + & tax_family_member ) class reform(Reform): diff --git a/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py b/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py index 38603128235..9968a6a8728 100644 --- a/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py +++ b/policyengine_us/reforms/aca/aca_ptc_simplified_bracket.py @@ -74,11 +74,16 @@ def formula(person, period, parameters): child_pays = person("aca_child_index", period) <= p_aca.max_child_count pays_aca_premium = is_aca_adult | child_pays + # The reform leaves 26 U.S.C. 36B(c)(1)(D) in place: someone + # another taxpayer can claim is outside the tax family. + tax_family_member = person("is_aca_tax_family_member", period) + return ( is_status_eligible & is_coverage_eligible & is_income_eligible & pays_aca_premium + & tax_family_member ) class reform(Reform): diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml new file mode 100644 index 00000000000..9d31224ee0b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml @@ -0,0 +1,238 @@ +- name: 2025 joint return with one claimable spouse and a child uses the one-person poverty line (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 2024 contiguous guideline for one person (parameters/gov/hhs/fpg.yaml). + aca_tax_family_size: 1 + aca_fpg: 15_060 + +- name: 2025 joint return with one claimable spouse and a child uses the one-person poverty line (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_tax_family_size: 1 + aca_fpg: 15_060 + +- name: 2025 joint return with neither spouse claimable and a child uses the three-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 15,060 + 2 * 5,380 + aca_fpg: 25_820 + +- name: 2025 single filer who can be claimed keeps the tax unit's one-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + households: + household: + members: [person_a] + state_code: TX + output: + # No tax family, so the line for the whole tax unit (1 person). + aca_tax_family_size: 0 + aca_fpg: 15_060 + +- name: 2025 joint return with both spouses claimable keeps the tax unit's two-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_tax_family_size: 0 + aca_fpg: 20_440 + +- name: 2026 joint return with one claimable spouse uses the 2025 one-person line (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_fpg: 15_650 + +- name: 2026 joint return with one claimable spouse uses the 2025 one-person line (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_fpg: 15_650 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml new file mode 100644 index 00000000000..e352b8a0826 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml @@ -0,0 +1,123 @@ +- name: 2025 joint return with one claimable spouse measures income against the one-person line (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 22,590 / 15,060 = 1.50 + aca_magi_fraction: 1.5 + +- name: 2025 joint return with one claimable spouse measures income against the one-person line (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_magi_fraction: 1.5 + +- name: 2025 joint return with neither spouse claimable measures income against the two-person line + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 22,590 / 20,440 = 1.105, truncated to 1.10 (Form 8962 Worksheet 2) + aca_magi_fraction: 1.1 + +- name: 2025 single filer who can be claimed keeps the one-person line + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + households: + household: + members: [person_a] + state_code: TX + output: + aca_magi_fraction: 1.5 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml new file mode 100644 index 00000000000..fd777a8eb45 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml @@ -0,0 +1,105 @@ +- name: 2025 below-FPL exception does not apply when the lawfully present spouse can be claimed elsewhere (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + immigration_status: TPS + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi_fraction: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 26 CFR 1.36B-2(b)(5): the taxpayer or a member of the taxpayer's + # family must be the lawfully present person. + aca_ptc_below_fpl_immigration_exception: false + +- name: 2025 below-FPL exception does not apply when the lawfully present spouse can be claimed elsewhere (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + immigration_status: TPS + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi_fraction: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc_below_fpl_immigration_exception: false + +- name: 2025 below-FPL exception applies when the lawfully present spouse is in the tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + immigration_status: TPS + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi_fraction: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_below_fpl_immigration_exception: true diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml new file mode 100644 index 00000000000..636b3ff9473 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml @@ -0,0 +1,233 @@ +- name: 2025 single filer who can be claimed elsewhere has tax family size 0 + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + # Form 8962 line 1 (2025 instructions p.8): if someone can claim you, or + # your spouse on a joint return, you or your spouse is not included in + # the tax family size. + aca_tax_family_size: 0 + tax_unit_size: 2 + +- name: 2025 joint return with one claimable spouse and two children has tax family size 1 (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child, child2] + state_code: TX + output: + # Form 8962 line 1 (2025 instructions p.8): if someone can claim you, or + # your spouse on a joint return, you or your spouse is not included in + # the tax family size. + aca_tax_family_size: 1 + tax_unit_size: 4 + +- name: 2025 joint return with one claimable spouse and two children has tax family size 1 (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child, child2] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child, child2] + state_code: TX + output: + aca_tax_family_size: 1 + tax_unit_size: 4 + +- name: 2025 joint return with both spouses claimable has tax family size 0 + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + aca_tax_family_size: 0 + +- name: 2025 joint return with neither spouse claimable counts the whole tax unit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child, child2] + state_code: TX + output: + aca_tax_family_size: 4 + +- name: 2025 single parent who cannot be claimed counts their child + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + aca_tax_family_size: 2 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml new file mode 100644 index 00000000000..6064d5ac59d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml @@ -0,0 +1,115 @@ +- name: "2025 joint return with one claimable spouse: the coverage family is the other spouse (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-3(b)(2): members of the taxpayer's family who enroll. + pays_aca_premium: [true, true, true] + is_aca_coverage_family_member: [false, true, false] + +- name: "2025 joint return with one claimable spouse: the coverage family is the other spouse (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + pays_aca_premium: [true, true, true] + is_aca_coverage_family_member: [true, false, false] + +- name: "2025 joint return with neither spouse claimable: a Medicaid-eligible child is not in the coverage family" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + is_aca_coverage_family_member: [true, true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml index 0c8789a0467..7d03af20362 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml @@ -495,3 +495,347 @@ has_qualifying_non_marketplace_health_coverage_at_interview: [true] pays_aca_premium: [false] is_aca_ptc_eligible: [false] + +- name: "2025 joint return with one claimable spouse: only the other spouse is eligible (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # IRC 36B(c)(1)(D); 26 CFR 1.36B-2(b)(3): someone another taxpayer may + # claim is not an applicable taxpayer. + aca_magi_fraction: 1.5 + is_aca_ptc_eligible: [false, true] + +- name: "2025 joint return with one claimable spouse: only the other spouse is eligible (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_magi_fraction: 1.5 + is_aca_ptc_eligible: [true, false] + +- name: 2025 single filer who can be claimed elsewhere is not eligible + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + households: + household: + members: [person_a] + state_code: TX + output: + is_aca_ptc_eligible: [false] + +- name: "2025 joint return with both spouses claimable: neither is eligible" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + is_aca_ptc_eligible: [false, false] + +- name: "2025 joint return with neither spouse claimable: both are eligible" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + is_aca_ptc_eligible: [true, true] + +- name: "2025 joint return with one claimable spouse: their child is not eligible either (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # IRC 152(b)(1): the return claims no dependents. + is_aca_ptc_eligible: [false, true, false] + +- name: "2025 joint return with one claimable spouse: their child is not eligible either (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + is_aca_ptc_eligible: [true, false, false] + +- name: "2025 couple below the two-person line but above the one-person line: the unclaimable spouse is eligible (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 18,000 / 15,060 = 1.195 for a tax family of one, so 100-400% FPL; + # the same couple with nobody claimable is at 0.88 and not eligible. + aca_magi_fraction: 1.19 + is_aca_ptc_eligible: [false, true] + +- name: "2025 couple below the two-person line but above the one-person line: the unclaimable spouse is eligible (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 18_000 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_magi_fraction: 1.19 + is_aca_ptc_eligible: [true, false] + +- name: 2025 couple below the two-person line with neither spouse claimable is not eligible + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_magi_fraction: 0.88 + is_aca_ptc_eligible: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml new file mode 100644 index 00000000000..7678861083c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -0,0 +1,209 @@ +- name: 2025 single filer who can be claimed elsewhere, and their child, are outside the tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2): the family is the taxpayer, both spouses on a + # joint return, except for individuals who qualify as a dependent of + # another taxpayer; IRC 152(b)(1): such a return claims no dependents. + is_aca_tax_family_member: [false, false] + +- name: 2025 joint return with one claimable spouse keeps only the other spouse in the tax family (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-1(d)(2): the family is the taxpayer, both spouses on a + # joint return, except for individuals who qualify as a dependent of + # another taxpayer; IRC 152(b)(1): such a return claims no dependents. + is_aca_tax_family_member: [false, true, false] + +- name: 2025 joint return with one claimable spouse keeps only the other spouse in the tax family (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + is_aca_tax_family_member: [true, false, false] + +- name: 2025 joint return with both spouses claimable has no tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + is_aca_tax_family_member: [false, false, false] + +- name: 2025 joint return with neither spouse claimable keeps everyone in the tax family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + is_aca_tax_family_member: [true, true, true] + +- name: 2025 dependent flagged as claimed elsewhere stays in the family of the return that claims them + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child: + age: 10 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + is_aca_tax_family_member: [true, true] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index e16d13d56e5..49a6222cef5 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -256,3 +256,473 @@ output: is_aca_ptc_eligible: [false] aca_ptc: 0 + +- name: "2025 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 150% FPL for one, 0% contribution: 6,000 - 0 + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 6000 + +- name: "2025 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 6000 + +- name: 2025 single filer who can be claimed elsewhere gets no credit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a] + state_code: TX + output: + # Form 8962 instructions: no one can claim you as a dependent. + aca_ptc: 0 + +- name: 2025 joint return with both spouses claimable gets no credit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc: 0 + +- name: "2025 joint return with neither spouse claimable: credit for both benchmarks" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 2025: 110% FPL for two, 0% contribution. + aca_tax_family_size: 2 + aca_ptc: 12_000 + +- name: "2026 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the head)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 150% FPL for one, 4.19% contribution: 6,000 - 0.0419 * 23,475 + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 5016.4 + +- name: "2026 joint return with one claimable spouse: credit for the other spouse's benchmark (the claimable spouse is the spouse)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + aca_ptc_slcsp: 6000 + aca_ptc: 5016.4 + +- name: 2026 single filer who can be claimed elsewhere gets no credit + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a] + state_code: TX + output: + # Form 8962 instructions: no one can claim you as a dependent. + aca_ptc: 0 + +- name: 2026 joint return with both spouses claimable gets no credit + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc: 0 + +- name: "2026 joint return with neither spouse claimable: credit for both benchmarks" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 2026: 23,475 / 21,150 = 110%, 2.1%: 12,000 - 0.021 * 23,475 + aca_tax_family_size: 2 + aca_ptc: 11507.03 + +- name: "2025 couple below the two-person line: the unclaimable spouse gets a credit (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 18,000 / 15,060 = 119% FPL for a family of one: 0% contribution. + aca_ptc: 6000 + +- name: "2025 couple below the two-person line: the unclaimable spouse gets a credit (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 18_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc: 6000 + +- name: 2025 couple below the two-person line with neither spouse claimable gets no credit + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 18_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml new file mode 100644 index 00000000000..5720af34beb --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -0,0 +1,372 @@ +- name: "2025 joint return with one claimable spouse: the benchmark covers only the other spouse (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-3(f): the benchmark offered to the coverage family; + # slcsp still counts every enrollee. + slcsp: 15_000 + aca_ptc_slcsp: 6000 + +- name: "2025 joint return with one claimable spouse: the benchmark covers only the other spouse (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + slcsp: 15_000 + aca_ptc_slcsp: 6000 + +- name: "2025 joint return with neither spouse claimable: the benchmark is slcsp" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + slcsp: 15_000 + aca_ptc_slcsp: 15_000 + +- name: "2025 single filer who can be claimed: no coverage family, no benchmark" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [person_a] + filing_status: SINGLE + aca_magi: 22_590 + households: + household: + members: [person_a] + state_code: TX + output: + slcsp: 6000 + aca_ptc_slcsp: 0 + +- name: "2025 joint return with both spouses claimable: no benchmark" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + slcsp: 12_000 + aca_ptc_slcsp: 0 + +- name: 2025 a supplied slcsp carries through when every enrollee is in the coverage family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + slcsp: 1200 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_slcsp: 1200 + +- name: "2025 NY joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the head)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp: 2166.0 + aca_ptc_slcsp: 760 + +- name: "2025 NY joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the spouse)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp: 2166.0 + aca_ptc_slcsp: 760 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the head)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp: 2135.6 + aca_ptc_slcsp: 760 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult benchmark (the claimable spouse is the spouse)" + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp: 2135.6 + aca_ptc_slcsp: 760 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml new file mode 100644 index 00000000000..80cf923afc6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml @@ -0,0 +1,285 @@ +- name: "2025 NY joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 NY joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 NY joint return with both spouses claimable: no coverage family tier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_category: INDIVIDUAL_AGE_RATED + +- name: "2025 NY joint return with neither spouse claimable: same tier as slcsp" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + +- name: "2025 VT joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 VT joint return with one claimable spouse and a child: the coverage family is one adult (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: ONE_ADULT + +- name: "2025 VT joint return with both spouses claimable: no coverage family tier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_category: INDIVIDUAL_AGE_RATED + +- name: "2025 VT joint return with neither spouse claimable: same tier as slcsp" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN + aca_ptc_slcsp_family_tier_category: TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml new file mode 100644 index 00000000000..e175f8f9941 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml @@ -0,0 +1,215 @@ +- name: "2025 NY joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.85 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 NY joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.85 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 NY joint return with both spouses claimable: zero multiplier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NY + state_code_str: NY + slcsp_rating_area: 3 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_multiplier: 0 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the head)" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.81 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 VT joint return with one claimable spouse and a child: one-adult multiplier (the claimable spouse is the spouse)" + period: 2025-01 + input: + people: + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + slcsp_family_tier_multiplier: 2.81 + aca_ptc_slcsp_family_tier_multiplier: 1.0 + +- name: "2025 VT joint return with both spouses claimable: zero multiplier" + period: 2025-01 + input: + people: + person_a: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + monthly_age: 30 + pays_aca_premium: true + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 5 + monthly_age: 5 + pays_aca_premium: true + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: VT + state_code_str: VT + slcsp_rating_area: 1 + slcsp_age_0: 760 + output: + aca_ptc_slcsp_family_tier_multiplier: 0 diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml index ba79805f0ff..737cdc45428 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml @@ -298,3 +298,76 @@ output: aca_ptc_below_fpl_immigration_exception: true is_aca_ptc_eligible: [true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the head) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_700_fpl_cliff.aca_ptc_700_fpl_cliff + input: + gov.contrib.aca.ptc_700_fpl_cliff.in_effect: true + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 46,950 / 15,650 = 300% FPL for a family of one. + aca_tax_family_size: 1 + is_aca_ptc_eligible: [false, true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the spouse) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_700_fpl_cliff.aca_ptc_700_fpl_cliff + input: + gov.contrib.aca.ptc_700_fpl_cliff.in_effect: true + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + is_aca_ptc_eligible: [true, false] diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml index 3f5846288f5..ce8da297e6e 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml @@ -115,3 +115,76 @@ aca_magi_fraction: 1.75 output: aca_required_contribution_percentage: 0.01 + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the head) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_additional_bracket.aca_ptc_additional_bracket + input: + gov.contrib.aca.ptc_additional_bracket.in_effect: true + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 46,950 / 15,650 = 300% FPL for a family of one. + aca_tax_family_size: 1 + is_aca_ptc_eligible: [false, true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the spouse) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_additional_bracket.aca_ptc_additional_bracket + input: + gov.contrib.aca.ptc_additional_bracket.in_effect: true + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + is_aca_ptc_eligible: [true, false] diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml index 93bb7d35bdd..820a2cb1b42 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml @@ -115,3 +115,76 @@ aca_magi_fraction: 6.0 output: aca_required_contribution_percentage: 0.20 + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the head) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_simplified_bracket.aca_ptc_simplified_bracket + input: + gov.contrib.aca.ptc_simplified_bracket.in_effect: true + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 46,950 / 15,650 = 300% FPL for a family of one. + aca_tax_family_size: 1 + is_aca_ptc_eligible: [false, true] + +- name: 2026 reform keeps a claimable spouse out of the tax family (the claimable spouse is the spouse) + period: 2026 + reforms: policyengine_us.reforms.aca.aca_ptc_simplified_bracket.aca_ptc_simplified_bracket + input: + gov.contrib.aca.ptc_simplified_bracket.in_effect: true + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 46_950 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_tax_family_size: 1 + is_aca_ptc_eligible: [true, false] diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py new file mode 100644 index 00000000000..2a882f1a066 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg + + +class aca_fpg(Variable): + value_type = float + entity = TaxUnit + label = "Federal poverty line for the premium tax credit tax family" + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#d_3", + # Line 1 (tax family size) and line 4 (poverty line). + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + ) + documentation = ( + "Form 8962 line 4: the prior year's poverty guideline for the tax " + "family size on line 1 (aca_tax_family_size). A return on which " + "every head and spouse can be claimed elsewhere has no tax family and " + "no applicable taxpayer; it keeps the guideline for the whole tax " + "unit, so readers that are not about the credit see the same value " + "as before." + ) + + def formula(tax_unit, period, parameters): + family_size = tax_unit("aca_tax_family_size", period) + size = where(family_size > 0, family_size, tax_unit("tax_unit_size", period)) + # The guideline year lags the tax year; the state group is read for + # the prior year, as the guideline lookup did before. + state_group = tax_unit.household("state_group_str", period.last_year) + return fpg(size, state_group, period, parameters, year_lag=1) diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py index 9464de7bdd2..d3780926dfe 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py @@ -6,23 +6,18 @@ class aca_magi_fraction(Variable): entity = TaxUnit label = "ACA-related modified AGI as fraction of prior-year FPL" documentation = ( - "ACA-related MAGI as fraction of federal poverty line. The first " - "reference documents the use of the prior-year FPL; the second " - "documents the truncation of the fraction." + "Household income as a fraction of the prior-year federal poverty " + "line for the premium tax credit tax family (Form 8962 line 5), " + "truncated to whole percentage points as Worksheet 2 instructs." ) reference = ( - dict( - title="2022 IRS Form 8962 (ACA PTC) instructions, Line 4", - href="https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=7", - ), - dict( - title="2022 IRS Form 8962 instructions, Line 5 Worksheet 2", - href="https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=8", - ), + # PDF pages 8-9: line 4 (prior-year poverty line for the line 1 + # family size) and line 5 Worksheet 2 (truncation). + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", ) definition_period = YEAR def formula(tax_unit, period, parameters): magi = max_(0, tax_unit("aca_magi", period)) - fpg = tax_unit("tax_unit_fpg", period.last_year) + fpg = tax_unit("aca_fpg", period) return np.floor(100 * magi / fpg) / 100 diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py b/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py index d12ea8c17cf..207731b8c21 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.py @@ -23,7 +23,13 @@ def formula(tax_unit, period, parameters): "is_medicaid_immigration_status_eligible", period ) + # 26 CFR 1.36B-2(b)(5): "the taxpayer or a member of the taxpayer's + # family" must be the lawfully present, Medicaid-ineligible person. + tax_family_member = person("is_aca_tax_family_member", period) qualifying_family_member = tax_unit.any( - non_citizen & aca_lawfully_present & medicaid_ineligible_due_to_status + tax_family_member + & non_citizen + & aca_lawfully_present + & medicaid_ineligible_due_to_status ) return in_effect & below_fpl & qualifying_family_member diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py new file mode 100644 index 00000000000..d8cea21ac2d --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class aca_tax_family_size(Variable): + value_type = int + entity = TaxUnit + label = "Premium tax credit tax family size" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#d_1", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d", + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + ) + documentation = ( + "Form 8962 line 1: the number of people in the premium tax credit " + "tax family. It equals the tax unit size unless a head or spouse can " + "be claimed as a dependent on another return, and is 0 when every " + "head and spouse can be. The shared tax_unit_size is unchanged." + ) + + adds = ["is_aca_tax_family_member"] diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py new file mode 100644 index 00000000000..022482995d0 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class is_aca_coverage_family_member(Variable): + value_type = bool + entity = Person + label = "Member of the premium tax credit coverage family" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2", + "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f_1", + ) + documentation = ( + "A Marketplace enrollee (pays_aca_premium) who is in the premium tax " + "credit tax family. 26 CFR 1.36B-3(b)(2) defines the coverage family " + "as the members of the taxpayer's family for whom the month is a " + "coverage month, and the benchmark plan in 1.36B-3(f) is the one " + "offered to the coverage family. An enrollee who can be claimed on " + "another return buys a plan but is not in it." + ) + + def formula(person, period, parameters): + return person("pays_aca_premium", period) & person( + "is_aca_tax_family_member", period + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py index b21baa83d3c..34e4cc60e67 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py @@ -6,6 +6,10 @@ class is_aca_ptc_eligible(Variable): entity = Person label = "Person is eligible for ACA premium tax credit and pays ACA premium" definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#c_1", + "https://www.law.cornell.edu/cfr/text/26/1.36B-2#b", + ) def formula(person, period, parameters): fstatus = person.tax_unit("filing_status", period) @@ -20,4 +24,14 @@ def formula(person, period, parameters): ) is_income_eligible = standard_income_eligible | below_fpl_exception - return person("pays_aca_premium", period) & ~separate & is_income_eligible + # Someone another taxpayer can claim is not an applicable taxpayer + # (26 U.S.C. 36B(c)(1)(D)) and is outside the tax family, along with + # the dependents of a return that has such a filer. + tax_family_member = person("is_aca_tax_family_member", period) + + return ( + person("pays_aca_premium", period) + & tax_family_member + & ~separate + & is_income_eligible + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py new file mode 100644 index 00000000000..ea43bed46d3 --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -0,0 +1,37 @@ +from policyengine_us.model_api import * + + +class is_aca_tax_family_member(Variable): + value_type = bool + entity = Person + label = "Member of the premium tax credit tax family" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#c_1_D", + "https://www.law.cornell.edu/uscode/text/26/152#b_1", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d_2", + "https://www.law.cornell.edu/cfr/text/26/1.36B-2#b_3", + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + ) + documentation = ( + "Whether the person is in the taxpayer's family for the premium tax " + "credit (Form 8962 line 1, tax family size). 26 CFR 1.36B-1(d)(2) " + "defines the family as the taxpayer, both spouses on a joint return, " + "'except for individuals who qualify as a dependent of another " + "taxpayer', plus the dependents the taxpayer claims. A head or spouse " + "who can be claimed on another return is therefore not a member " + "(IRC 36B(c)(1)(D), 26 CFR 1.36B-2(b)(3)), and on a joint return the " + "other spouse still is. A return with a filer who can be claimed " + "elsewhere claims no dependents (IRC 152(b)(1)), so its dependents " + "are members only when no head or spouse can be claimed elsewhere. " + "The claimed flag of a dependent on this return does not matter: this " + "return claims them." + ) + + def formula(person, period, parameters): + filer = person("is_tax_unit_head_or_spouse", period) + claimed_elsewhere = person("claimed_as_dependent_on_another_return", period) + return_has_claimed_filer = person.tax_unit( + "head_or_spouse_is_dependent_elsewhere", period + ) + return where(filer, ~claimed_elsewhere, ~return_has_claimed_filer) diff --git a/policyengine_us/variables/gov/aca/ptc/aca_ptc.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc.py index 76c9fbe2464..79a7ff5a45a 100644 --- a/policyengine_us/variables/gov/aca/ptc/aca_ptc.py +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc.py @@ -19,7 +19,9 @@ def formula(tax_unit, period, parameters): return 0 def formula_2018(tax_unit, period, parameters): - plan_cost = tax_unit("slcsp", period) + # The benchmark for the coverage family (26 CFR 1.36B-3(f)), which + # leaves out an enrollee who can be claimed on another return. + plan_cost = tax_unit("aca_ptc_slcsp", period) income = tax_unit("aca_magi", period) applicable_figure = tax_unit("aca_required_contribution_percentage", period) # IRC § 36B(a) conditions the PTC on filing a return (Form 8962). diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py new file mode 100644 index 00000000000..ea2eaf3ae9a --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class aca_ptc_slcsp(Variable): + value_type = float + entity = TaxUnit + label = "Benchmark silver-plan premium for the premium tax credit" + unit = USD + definition_period = MONTH + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#b_3_B", + "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f", + ) + documentation = ( + "The second-lowest-cost silver plan premium offered to the coverage " + "family (26 CFR 1.36B-3(f)): the enrollees who are in the premium tax " + "credit tax family. When every enrollee on the return is in it, this " + "is slcsp itself, read directly so a supplied slcsp carries through. " + "When an enrollee is not (one who can be claimed on another return, " + "or a dependent of a return with such a filer), it is the age-curve " + "premiums of the coverage family members or, where family tiers " + "apply, the tier premium for them. slcsp keeps every enrollee, since " + "everyone still buys a plan." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + enrolled = person("pays_aca_premium", period.this_year) + covered = person("is_aca_coverage_family_member", period.this_year) + every_enrollee_covered = ~tax_unit.any(enrolled & ~covered) + age_curve = tax_unit.sum( + covered * person("slcsp_age_curve_amount_person", period) + ) + family_tier = tax_unit.household("slcsp_age_0", period) * tax_unit( + "aca_ptc_slcsp_family_tier_multiplier", period + ) + coverage_family_benchmark = age_curve + family_tier + return where( + every_enrollee_covered, + tax_unit("slcsp", period), + coverage_family_benchmark, + ) diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py new file mode 100644 index 00000000000..cd6475506cb --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_category.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( + FamilyTierCategory, + family_tier_category, +) + + +class aca_ptc_slcsp_family_tier_category(Variable): + value_type = Enum + entity = TaxUnit + possible_values = FamilyTierCategory + default_value = FamilyTierCategory.INDIVIDUAL_AGE_RATED + definition_period = MONTH + label = "ACA family tier category for the premium tax credit coverage family" + defined_for = "slcsp_family_tier_applies" + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f" + documentation = ( + "The family tier (New York and Vermont) of the coverage family's " + "benchmark plan: slcsp_family_tier_category counted over coverage " + "family members only, so an enrollee who can be claimed on another " + "return does not move the tier." + ) + + def formula(tax_unit, period, parameters): + covered = tax_unit.members("is_aca_coverage_family_member", period.this_year) + return family_tier_category(tax_unit, period, parameters, covered) diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py new file mode 100644 index 00000000000..4edacc5d1a6 --- /dev/null +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp_family_tier_multiplier.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_multiplier import ( + family_tier_multiplier, +) + + +class aca_ptc_slcsp_family_tier_multiplier(Variable): + value_type = float + entity = TaxUnit + label = "ACA family tier multiplier for the premium tax credit coverage family" + unit = "/1" + definition_period = MONTH + defined_for = "slcsp_family_tier_applies" + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f" + + def formula(tax_unit, period, parameters): + family_category = tax_unit("aca_ptc_slcsp_family_tier_category", period) + covered = tax_unit.members("is_aca_coverage_family_member", period.this_year) + return family_tier_multiplier( + tax_unit, period, parameters, family_category, covered + ) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py index 147aa042035..1b9a354fdd4 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py @@ -10,6 +10,80 @@ class FamilyTierCategory(Enum): CHILD_ONLY = "Child_only" +def family_tier_category(tax_unit, period, parameters, enrolled): + """ + Determine the family tier category for premium calculation in states + that use family tiers instead of individual age rating, counting only + the members flagged in `enrolled` (a person-level boolean array). + + Categories for both NY and VT: + - One_adult + - Two_adults + - One_adult_and_one_or_more_children + - Two_adults_and_one_or_more_children + - Child_only (only for NY) + """ + # Get inputs + state_code = tax_unit.household("state_code_str", period) + + person = tax_unit.members + dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + non_dependent_adult_count = tax_unit.sum(enrolled & ~dependent_child) + anchored_child_count = tax_unit.sum(dependent_child) + + under_child_only_age = ( + person("monthly_age", period) <= parameters(period).gov.aca.slcsp.max_child_age + ) + child_only_child = enrolled & under_child_only_age + unanchored_adult_count = tax_unit.sum(enrolled & ~child_only_child) + unanchored_child_count = tax_unit.sum(child_only_child) + has_adult_anchor = non_dependent_adult_count > 0 + eligible_adult_count = where( + has_adult_anchor, + non_dependent_adult_count, + unanchored_adult_count, + ) + eligible_child_count = where( + has_adult_anchor, + anchored_child_count, + unanchored_child_count, + ) + + # Common conditions for both states + one_adult_no_children = (eligible_adult_count == 1) & (eligible_child_count == 0) + two_plus_adults_no_children = (eligible_adult_count >= 2) & ( + eligible_child_count == 0 + ) + one_adult_with_children = (eligible_adult_count == 1) & (eligible_child_count > 0) + two_plus_adults_with_children = (eligible_adult_count >= 2) & ( + eligible_child_count > 0 + ) + + # NY-specific condition (child-only households) + ny_child_only = ( + (state_code == "NY") & (eligible_adult_count == 0) & (eligible_child_count > 0) + ) + + return select( + [ + # Apply conditions only to family tier states + ny_child_only, + one_adult_no_children, + two_plus_adults_no_children, + one_adult_with_children, + two_plus_adults_with_children, + ], + [ + FamilyTierCategory.CHILD_ONLY, + FamilyTierCategory.ONE_ADULT, + FamilyTierCategory.TWO_ADULTS, + FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + ], + default=FamilyTierCategory.INDIVIDUAL_AGE_RATED, + ) + + class slcsp_family_tier_category(Variable): value_type = Enum entity = TaxUnit @@ -20,81 +94,5 @@ class slcsp_family_tier_category(Variable): defined_for = "slcsp_family_tier_applies" def formula(tax_unit, period, parameters): - """ - Determine the family tier category for premium calculation in states - that use family tiers instead of individual age rating. - - Categories for both NY and VT: - - One_adult - - Two_adults - - One_adult_and_one_or_more_children - - Two_adults_and_one_or_more_children - - Child_only (only for NY) - """ - # Get inputs - state_code = tax_unit.household("state_code_str", period) - - person = tax_unit.members - pays_premium = person("pays_aca_premium", period) - dependent_child = person("is_aca_family_tier_dependent_child", period) - non_dependent_adult_count = tax_unit.sum(pays_premium & ~dependent_child) - anchored_child_count = tax_unit.sum(dependent_child) - - under_child_only_age = ( - person("monthly_age", period) - <= parameters(period).gov.aca.slcsp.max_child_age - ) - child_only_child = pays_premium & under_child_only_age - unanchored_adult_count = tax_unit.sum(pays_premium & ~child_only_child) - unanchored_child_count = tax_unit.sum(child_only_child) - has_adult_anchor = non_dependent_adult_count > 0 - eligible_adult_count = where( - has_adult_anchor, - non_dependent_adult_count, - unanchored_adult_count, - ) - eligible_child_count = where( - has_adult_anchor, - anchored_child_count, - unanchored_child_count, - ) - - # Common conditions for both states - one_adult_no_children = (eligible_adult_count == 1) & ( - eligible_child_count == 0 - ) - two_plus_adults_no_children = (eligible_adult_count >= 2) & ( - eligible_child_count == 0 - ) - one_adult_with_children = (eligible_adult_count == 1) & ( - eligible_child_count > 0 - ) - two_plus_adults_with_children = (eligible_adult_count >= 2) & ( - eligible_child_count > 0 - ) - - # NY-specific condition (child-only households) - ny_child_only = ( - (state_code == "NY") - & (eligible_adult_count == 0) - & (eligible_child_count > 0) - ) - - return select( - [ - # Apply conditions only to family tier states - ny_child_only, - one_adult_no_children, - two_plus_adults_no_children, - one_adult_with_children, - two_plus_adults_with_children, - ], - [ - FamilyTierCategory.CHILD_ONLY, - FamilyTierCategory.ONE_ADULT, - FamilyTierCategory.TWO_ADULTS, - FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - ], - default=FamilyTierCategory.INDIVIDUAL_AGE_RATED, - ) + enrolled = tax_unit.members("pays_aca_premium", period) + return family_tier_category(tax_unit, period, parameters, enrolled) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py index 64f85feb3d7..194ac280ae0 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py @@ -4,6 +4,60 @@ ) +def family_tier_multiplier(tax_unit, period, parameters, family_category, enrolled): + """ + Family tier premium multiplier for `family_category`, counting only the + members flagged in `enrolled` (a person-level boolean array). + """ + p = parameters(period).gov.aca.family_tier_ratings + state_code = tax_unit.household("state_code", period) + in_ny = state_code == state_code.possible_values.NY + + one_adult = where(in_ny, p.ny.ONE_ADULT, p.vt.ONE_ADULT) + two_adults = where(in_ny, p.ny.TWO_ADULTS, p.vt.TWO_ADULTS) + one_adult_with_children = where( + in_ny, + p.ny.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + p.vt.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + ) + two_adults_with_children = where( + in_ny, + p.ny.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + p.vt.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + ) + child_only = where(in_ny, p.ny.CHILD_ONLY, 0) + + base_multiplier = select( + [ + family_category == FamilyTierCategory.ONE_ADULT, + family_category == FamilyTierCategory.TWO_ADULTS, + family_category == FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, + family_category == FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, + family_category == FamilyTierCategory.CHILD_ONLY, + ], + [ + one_adult, + two_adults, + one_adult_with_children, + two_adults_with_children, + child_only, + ], + default=0, + ) + + person = tax_unit.members + dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + adult_count = tax_unit.sum(enrolled & ~dependent_child) + extra_adults = max_(adult_count - 2, 0) + age_29_child = enrolled & person("is_aca_ny_age_29_dependent_child", period) + age_29_multiplier = where( + tax_unit.any(age_29_child), + parameters(period).gov.aca.ny_age_29_dependent_child_tier_multiplier, + 1, + ) + return base_multiplier * age_29_multiplier + extra_adults * one_adult + + class slcsp_family_tier_multiplier(Variable): value_type = float entity = TaxUnit @@ -14,53 +68,7 @@ class slcsp_family_tier_multiplier(Variable): def formula(tax_unit, period, parameters): family_category = tax_unit("slcsp_family_tier_category", period) - p = parameters(period).gov.aca.family_tier_ratings - state_code = tax_unit.household("state_code", period) - in_ny = state_code == state_code.possible_values.NY - - one_adult = where(in_ny, p.ny.ONE_ADULT, p.vt.ONE_ADULT) - two_adults = where(in_ny, p.ny.TWO_ADULTS, p.vt.TWO_ADULTS) - one_adult_with_children = where( - in_ny, - p.ny.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - p.vt.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - ) - two_adults_with_children = where( - in_ny, - p.ny.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - p.vt.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - ) - child_only = where(in_ny, p.ny.CHILD_ONLY, 0) - - base_multiplier = select( - [ - family_category == FamilyTierCategory.ONE_ADULT, - family_category == FamilyTierCategory.TWO_ADULTS, - family_category - == FamilyTierCategory.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN, - family_category - == FamilyTierCategory.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN, - family_category == FamilyTierCategory.CHILD_ONLY, - ], - [ - one_adult, - two_adults, - one_adult_with_children, - two_adults_with_children, - child_only, - ], - default=0, - ) - - person = tax_unit.members - pays_premium = person("pays_aca_premium", period) - dependent_child = person("is_aca_family_tier_dependent_child", period) - adult_count = tax_unit.sum(pays_premium & ~dependent_child) - extra_adults = max_(adult_count - 2, 0) - age_29_child = person("is_aca_ny_age_29_dependent_child", period) - age_29_multiplier = where( - tax_unit.any(age_29_child), - parameters(period).gov.aca.ny_age_29_dependent_child_tier_multiplier, - 1, + enrolled = tax_unit.members("pays_aca_premium", period) + return family_tier_multiplier( + tax_unit, period, parameters, family_category, enrolled ) - return base_multiplier * age_29_multiplier + extra_adults * one_adult From 28212117a454b7df52cfba1846fef903ca3e152e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 12:36:17 -0400 Subject: [PATCH 2/7] Apply each state premium program's own rule to an enrollee outside the tax family Massachusetts, Connecticut, Colorado, Maryland, California, New Mexico and Vermont require each enrollee to be eligible for the federal credit, so the gate flows through is_aca_ptc_eligible and their wraps use the coverage family's benchmark. New Mexico's Middle Income Household component and New Jersey Health Plan Savings require every federal criterion except income, so they now read the tax family too. Washington's Cascade Care Savings pays its non-federally-subsidized amount (Group 2) to a QHP-eligible enrollee without a federal subsidy, so a claimable enrollee moves from Group 1 to Group 2. Co-Authored-By: Claude Opus 5.5 --- .../premium_subsidy/ca_premium_subsidy.yaml | 120 +++++++++++++ .../co_premium_assistance.yaml | 162 +++++++++++++++++ .../ct_covered_connecticut.yaml | 162 +++++++++++++++++ .../hic/connector_care/ma_connector_care.yaml | 162 +++++++++++++++++ .../ma_connector_care_member_eligible.yaml | 76 ++++++++ .../md_premium_assistance.yaml | 162 +++++++++++++++++ .../gov/states/nj/dobi/njhps/nj_njhps.yaml | 162 +++++++++++++++++ .../dobi/njhps/nj_njhps_member_eligible.yaml | 76 ++++++++ .../nm_premium_assistance.yaml | 162 +++++++++++++++++ .../nm_premium_assistance_mih_eligible.yaml | 123 +++++++++++++ .../vt_premium_assistance.yaml | 158 +++++++++++++++++ .../wa_cascade_care_savings.yaml | 163 ++++++++++++++++++ ...wa_cascade_care_savings_member_amount.yaml | 77 +++++++++ ..._cascade_care_savings_member_eligible.yaml | 76 ++++++++ .../premium_subsidy/ca_premium_subsidy.py | 7 +- .../ca_premium_subsidy_eligible.py | 7 +- .../co_premium_assistance.py | 9 +- .../ct_covered_connecticut.py | 17 +- .../hic/connector_care/ma_connector_care.py | 8 +- .../md_premium_assistance.py | 7 +- .../gov/states/nj/dobi/njhps/nj_njhps.py | 4 +- .../nj/dobi/njhps/nj_njhps_member_eligible.py | 12 +- .../nm_premium_assistance.py | 8 +- .../nm_premium_assistance_mih_eligible.py | 6 +- .../vt_premium_assistance.py | 12 +- .../wa_cascade_care_savings_member_amount.py | 19 +- ...wa_cascade_care_savings_member_eligible.py | 24 ++- 27 files changed, 1939 insertions(+), 42 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 889da75d6d7..34561143831 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -504,3 +504,123 @@ output: ca_premium_subsidy_eligible: true ca_premium_subsidy: 3_935 + +- name: 2026 CA joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CA + output: + # Program Design (c)(2), (d)(7)(B). 22,000 / 15,650 = 140% FPL; + # federal 3.5724%: aca_ptc = 6,000 - 785.92 = 5,214.08; CA 0%: + # 6,000 - 5,214.08 = 785.92. + ca_premium_subsidy: 785.92 + +- name: 2026 CA joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CA + output: + ca_premium_subsidy: 785.92 + +- name: 2026 CA joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CA + output: + # 104% FPL, 2.1%: aca_ptc = 12,000 - 462 = 11,538; CA 0%: 462. + ca_premium_subsidy: 462 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml index ec7b5568aeb..fdd1995fa79 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml @@ -681,3 +681,165 @@ co_premium_assistance_eligible: true co_premium_assistance: 960 assigned_co_premium_assistance: 0 + +- name: 2026 CO joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # C.R.S. 10-16-1205(1)(b)(II): payments for Coloradans who receive the + # premium tax credit. One member: min(12 * 80, 6,000 - 5,016.40) = 960. + co_premium_assistance: 960 + +- name: 2026 CO joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CO + output: + co_premium_assistance: 960 + +- name: 2026 CO joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + co_premium_assistance: 492.97 + +- name: 2026 CO joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # Neither spouse is in a federal tax family. + co_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml index 8284416c1e3..22c48b87423 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml @@ -371,3 +371,165 @@ state_code: CT output: ct_covered_connecticut: 500 # verified: negative income does not disturb the injected residual + +- name: 2026 CT joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CT + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # Public Act 21-2, Section 16(b)(1): enrollees must be eligible for premium + # and cost-sharing subsidies. 6,000 - 5,016.40 = 983.60. + ct_covered_connecticut: 983.6 + +- name: 2026 CT joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CT + output: + ct_covered_connecticut: 983.6 + +- name: 2026 CT joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CT + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + ct_covered_connecticut: 492.97 + +- name: 2026 CT joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CT + output: + # Neither spouse is in a federal tax family. + ct_covered_connecticut: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml index cf046cd4d31..b52405c1e14 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml @@ -566,3 +566,165 @@ state_code: MA output: ma_connector_care: 500 + +- name: 2026 MA joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # 956 CMR 12.04(3)(a)2: ConnectorCare needs APTC eligibility, so only the + # other spouse counts. Band 2A ($0): 6,000 - 5,016.40 - 0 = 983.60. + ma_connector_care: 983.6 + +- name: 2026 MA joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: MA + output: + ma_connector_care: 983.6 + +- name: 2026 MA joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + ma_connector_care: 492.97 + +- name: 2026 MA joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + # Neither spouse is in a federal tax family. + ma_connector_care: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml index 211a5ca2dc7..27c4ffd05fa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml @@ -57,3 +57,79 @@ ma_connector_care_member_eligible: true person2: ma_connector_care_member_eligible: false + +- name: 2026 MA a spouse who can be claimed elsewhere is not a ConnectorCare member (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MA + output: + ma_connector_care_member_eligible: [false, true] + +- name: 2026 MA a spouse who can be claimed elsewhere is not a ConnectorCare member (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: MA + output: + ma_connector_care_member_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml index 785e626bc21..fe6090f2e5b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml @@ -723,3 +723,165 @@ state_code: VA output: md_premium_assistance: [868.57, 0] # MD unit derived > 0, VA unit masked to 0 by defined_for + +- name: 2026 MD joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MD + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # COMAR 14.35.21.03A(1): members of an APTC-eligible tax household. + # 0% target at 150% FPL: 6,000 - 5,016.40 = 983.60. + md_premium_assistance: 983.6 + +- name: 2026 MD joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: MD + output: + md_premium_assistance: 983.6 + +- name: 2026 MD joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MD + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + md_premium_assistance: 492.97 + +- name: 2026 MD joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: MD + output: + # Neither spouse is in a federal tax family. + md_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml index 5ce199518f4..9982d762b4a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml @@ -792,3 +792,165 @@ state_code: NJ output: nj_njhps: 0 + +- name: 2026 NJ joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # NJHPS goes to enrollees otherwise eligible for APTC: one member at + # $20 PMPM (138-150% FPL) = 240; nobody claimable is 110% FPL, below 138%. + nj_njhps: 240 + +- name: 2026 NJ joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NJ + output: + nj_njhps: 240 + +- name: 2026 NJ joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + nj_njhps: 0 + +- name: 2026 NJ joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + # Neither spouse is in a federal tax family. + nj_njhps: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml index 0aadc7215a3..a17d1e77e50 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml @@ -267,3 +267,79 @@ state_code: NJ output: nj_njhps_member_eligible: false + +- name: 2026 NJ a spouse who can be claimed elsewhere is not an NJHPS member (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NJ + output: + nj_njhps_member_eligible: [false, true] + +- name: 2026 NJ a spouse who can be claimed elsewhere is not an NJHPS member (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NJ + output: + nj_njhps_member_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml index 7852514f67b..3b634cb0983 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml @@ -885,3 +885,165 @@ # Derived SLCSP from the Bernalillo County rating area yields a positive base # top-up. Value is data-driven from the premium table. nm_premium_assistance: 2_645.60 + +- name: 2026 NM joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # MAP manual: consumers must qualify for the federal premium tax credit. + # Gross-up to 200% FPL, 0% target: 6,000 * 1.1 - 5,016.40 = 1,583.60. + nm_premium_assistance: 1583.6 + +- name: 2026 NM joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NM + output: + nm_premium_assistance: 1583.6 + +- name: 2026 NM joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + nm_premium_assistance: 1692.97 + +- name: 2026 NM joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # Neither spouse is in a federal tax family. + nm_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml index 9dc6fda92fb..4a86773a91f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml @@ -237,3 +237,126 @@ state_code: NM output: nm_premium_assistance_mih_eligible: false + +- name: "2026 NM above 400% FPL for a tax family of one: the other spouse qualifies for the MIH component (the claimable spouse is the head)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 70_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # 70,000 / 15,650 = 447% FPL; 6,000 - 0.085 * 70,000 = 50. + aca_magi_fraction: 4.47 + nm_premium_assistance_mih_eligible: true + nm_premium_assistance: 50 + +- name: "2026 NM above 400% FPL for a tax family of one: the other spouse qualifies for the MIH component (the claimable spouse is the spouse)" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 70_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: NM + output: + aca_magi_fraction: 4.47 + nm_premium_assistance_mih_eligible: true + nm_premium_assistance: 50 + +- name: "2026 NM above 400% FPL with both spouses claimable: no MIH component" + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 70_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: NM + output: + # The Addendum requires every federal PTC criterion except income. + nm_premium_assistance_mih_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml index 52b98db3dc1..0623d929169 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml @@ -406,3 +406,161 @@ state_code: VT output: vt_premium_assistance: 0 + +- name: 2026 VT joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: VT + slcsp_age_0: 6000 + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # 33 V.S.A. 1812(a)(1). Family tier ONE_ADULT: benchmark 6,000. + # min(23,475 * 0.015, 6,000 - 5,016.40) = 352.13. + vt_premium_assistance: 352.13 + +- name: 2026 VT joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: VT + slcsp_age_0: 6000 + output: + vt_premium_assistance: 352.13 + +- name: 2026 VT joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: VT + slcsp_age_0: 6000 + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + vt_premium_assistance: 352.13 + +- name: 2026 VT joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: VT + slcsp_age_0: 6000 + output: + # Neither spouse is in a federal tax family. + vt_premium_assistance: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml index 986ed843771..5d2dd12d03d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml @@ -749,3 +749,166 @@ state_code: WA output: wa_cascade_care_savings: 3_000 + +- name: 2026 WA joint return with one claimable spouse (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # Tax family of one: 23,475 / 15,650 = 150% FPL; aca_ptc_slcsp 6,000; + # aca_ptc = 6,000 - 0.0419 * 23,475 = 5,016.40. + # Group 2 ($250 PMPM) for the claimable spouse, who is QHP-eligible but + # has no federal subsidy, and Group 1 ($55) for the other: 3,000 + 660; + # cap = 12,000 - 5,016.40 = 6,983.60 (both enrollees' net premium). + wa_cascade_care_savings: 3660 + +- name: 2026 WA joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: WA + output: + wa_cascade_care_savings: 3660 + +- name: 2026 WA joint return with neither spouse claimable + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # Tax family of two: 110% FPL; aca_ptc = 12,000 - 0.021 * 23,475 = 11,507.03. + wa_cascade_care_savings: 492.97 + +- name: 2026 WA joint return with both spouses claimable gets nothing from the wrap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # Both spouses are Group 2: 2 * 12 * 250 = 6,000, below the cap of 12,000. + wa_cascade_care_savings: 6000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml index 0352869ffe8..2caf6ea14e0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml @@ -154,3 +154,80 @@ state_code: WA output: wa_cascade_care_savings_member_amount: [660, 3_000, 0, 0] + +- name: 2026 WA the claimable spouse gets the Group 2 amount and the other the Group 1 amount (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + # 12 * 250 and 12 * 55. + wa_cascade_care_savings_member_amount: [3000, 660] + +- name: 2026 WA the claimable spouse gets the Group 2 amount and the other the Group 1 amount (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: WA + output: + wa_cascade_care_savings_member_amount: [660, 3000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml index 15b9fdf1d30..e6343dfebfc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml @@ -173,3 +173,79 @@ state_code: WA output: wa_cascade_care_savings_member_eligible: [true, true, false, false] + +- name: 2026 WA a spouse who can be claimed elsewhere is a Group 2 member (the claimable spouse is the head) + period: 2026 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: WA + output: + wa_cascade_care_savings_member_eligible: [true, true] + +- name: 2026 WA a spouse who can be claimed elsewhere is a Group 2 member (the claimable spouse is the spouse) + period: 2026 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 23_475 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: WA + output: + wa_cascade_care_savings_member_eligible: [true, true] diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py index 32ad4395a41..213e55ae882 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.py @@ -31,9 +31,10 @@ def formula(tax_unit, period, parameters): # Clamp income to zero so a negative ACA MAGI cannot inflate the top-up # above the benchmark - APTC residual via the -pct*income term. income = max_(tax_unit("aca_magi", period), 0) - # slcsp is a MONTH-period variable; core sums the 12 months when - # called from this YEAR formula. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the coverage family (Program Design (d)(5) points + # to 26 CFR 1.36B-3(f)); a MONTH variable that core sums over the + # year here. + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) ca_pct = tax_unit("ca_premium_subsidy_applicable_percentage", period) # Eligibility is enforced by defined_for = ca_premium_subsidy_eligible. diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py index e9fd47b9eaf..9e873bcd408 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py @@ -42,7 +42,10 @@ def formula(tax_unit, period, parameters): # must be a filer. This gate replaces reliance on the federal 100% FPL # floor exception: the federal PTC gate admits a below-poverty # immigration exception that the applicable-return-filer definition - # excludes. Dependents claimed by another filer do not form separate - # claiming units in the model, so the dependent exclusion is structural. + # excludes. Program Design (d)(7)(B) allows no assistance to an + # individual claimed as a dependent of another return filer; the + # federal gate already leaves such a person out of the tax family + # (26 U.S.C. 36B(c)(1)(D)), and Program Design (c)(2) requires every + # federal APTC eligibility requirement. is_filer = tax_unit("tax_unit_is_filer", period) return in_effect & aptc_eligible & income_eligible & is_filer diff --git a/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py b/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py index 53bf7dc4fd2..3905017f842 100644 --- a/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py +++ b/policyengine_us/variables/gov/states/co/doi/premium_assistance/co_premium_assistance.py @@ -60,9 +60,12 @@ def formula(tax_unit, period, parameters): p.amount.first_member + p.amount.additional_member * (n - 1), 0, ) - # slcsp is a MONTH-period variable; core sums the 12 months when called - # from this YEAR formula. Approximates the enrolled-plan premium. - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # core sums over the year here; it approximates the eligible members' + # enrolled-plan premiums. C.R.S. 10-16-1205(1)(b)(II) pays carriers + # for Coloradans who receive the premium tax credit, so an enrollee + # outside the federal tax family is not counted. + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) residual = max_(0, slcsp_annual - aca_ptc) annual = min_(MONTHS_IN_YEAR * flat_monthly, residual) diff --git a/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py b/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py index 1ac3523df95..6f9ea164e88 100644 --- a/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py +++ b/policyengine_us/variables/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.py @@ -17,9 +17,11 @@ class ct_covered_connecticut(Variable): "Covered Connecticut pays the enrollee's residual benchmark silver-plan " "premium after the federal advance premium tax credit, driving the net " "premium to $0 for eligible enrollees at or below 175% of the federal " - "poverty line. The benchmark second-lowest-cost silver plan premium " - "(slcsp) proxies the benchmark plan and is annualized from its monthly " - "definition. The subsidy is additive on top of the federal APTC and is " + "poverty line. The premium tax credit's benchmark second-lowest-cost " + "silver plan premium for the coverage family (aca_ptc_slcsp) proxies " + "the benchmark plan and is annualized from its monthly definition; it " + "leaves out an enrollee who is not eligible for the federal subsidies " + "because they can be claimed on another return. The subsidy is additive on top of the federal APTC and is " "capped implicitly at the benchmark premium because the federal APTC " "never exceeds it, so the residual is never negative. The program also " "zeroes cost-sharing and adds dental and non-emergency medical " @@ -30,9 +32,12 @@ class ct_covered_connecticut(Variable): ) def formula(tax_unit, period, parameters): - # slcsp is a MONTH-period variable; add() annualizes it to match the - # YEAR-period aca_ptc so both sides of the residual are annual. - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # add() annualizes to match the YEAR-period aca_ptc. Public Act 21-2, + # Section 16(b)(1), covers only people eligible for premium and + # cost-sharing subsidies, so an enrollee outside the federal tax + # family (one who can be claimed on another return) is left out. + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) # Residual benchmark premium after the federal APTC. The max_(0, ...) # floor is defensive and redundant in the baseline, where aca_ptc never diff --git a/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py b/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py index 154b1faf3d2..45f1f0deb3d 100644 --- a/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py +++ b/policyengine_us/variables/gov/states/ma/hic/connector_care/ma_connector_care.py @@ -48,9 +48,11 @@ def formula(tax_unit, period, parameters): magi_fraction = tax_unit("aca_magi_fraction", period) per_person_monthly = p.enrollee_premium.calc(magi_fraction) enrollee_contribution = MONTHS_IN_YEAR * per_person_monthly * n_members - # SLCSP is a MONTH variable summed to the year to proxy the benchmark - # ConnectorCare plan; the full APTC is netted per 956 CMR 12.04(3)(c). - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family (a MONTH variable + # summed to the year) proxies the benchmark ConnectorCare plan of the + # APTC-eligible enrollees (956 CMR 12.04(3)(a)2); the full APTC is + # netted per 956 CMR 12.04(3)(c). + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) subsidy = max_(0, slcsp_annual - aca_ptc - enrollee_contribution) eligible = tax_unit("ma_connector_care_eligible", period) diff --git a/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py b/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py index 28b2835f2c9..e67ff51f223 100644 --- a/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py +++ b/policyengine_us/variables/gov/states/md/mhbe/premium_assistance/md_premium_assistance.py @@ -46,9 +46,10 @@ def formula(tax_unit, period, parameters): md_percentage = tax_unit( "md_premium_assistance_target_contribution_percentage", period ) - # slcsp is a MONTH-period variable; core sums the 12 months when - # called from this YEAR formula. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # core sums over the year here: COMAR 14.35.21.03A(1) limits the + # subsidy to members of an APTC-eligible tax filer's tax household. + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) # Premium balance remaining after the federal APTC (COMAR .04B). premium_after_aptc = max_(0, slcsp - aca_ptc) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py index 09adfb9943c..97c660c1ad8 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py @@ -49,7 +49,9 @@ def formula(tax_unit, period, parameters): # plan premium. NJHPS is added atop APTC and cannot lower the net # premium below zero, so it is capped at the post-APTC residual. In the # 400-600% band aca_ptc is 0, so the residual equals slcsp_annual. - slcsp_annual = add(tax_unit, period, ["slcsp"]) + # The benchmark is the federal coverage family's: NJHPS members must + # be in a tax household otherwise eligible for APTC. + slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) residual = max_(0, slcsp_annual - aca_ptc) return min_(base_annual, residual) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py index 6be048c756f..f46e8605147 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py @@ -13,7 +13,10 @@ class nj_njhps_member_eligible(Variable): ) documentation = ( "A person is a New Jersey Health Plan Savings enrollee when they pay an " - "ACA marketplace premium, do not file a separate return, and household " + "ACA marketplace premium, are in the federal premium tax credit tax " + "family (NJHPS goes to enrollees otherwise eligible for APTC, so not " + "someone another taxpayer can claim), do not file a separate return, " + "and household " "income (ACA MAGI per 26 U.S.C. 36B(d)(2)) falls within the 138-600% " "FPL NJHPS band. This deliberately does NOT reuse is_aca_ptc_eligible: " "that variable embeds the federal 400% FPL income cliff (the PY2026 " @@ -45,7 +48,12 @@ def formula(person, period, parameters): # income under the federal PTC floor of 100% FPL, and within_band # already starts at the 138% FPL NJHPS floor. pays_premium = person("pays_aca_premium", period) + # NJHPS goes to enrollees otherwise eligible for APTC, so a person + # outside the federal tax family (one who can be claimed on another + # return, or a dependent of a return with such a filer) is not a + # member (26 U.S.C. 36B(c)(1)(D) is not an income test). + tax_family_member = person("is_aca_tax_family_member", period) fstatus = person.tax_unit("filing_status", period) not_separate = fstatus != fstatus.possible_values.SEPARATE within_band = (magi_fraction >= p.fpl_floor) & (magi_fraction <= p.fpl_limit) - return pays_premium & not_separate & within_band + return pays_premium & tax_family_member & not_separate & within_band diff --git a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py index df33b423c65..877bbee18e6 100644 --- a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py +++ b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance.py @@ -36,9 +36,11 @@ def formula(tax_unit, period, parameters): # Clamp income to zero so a negative ACA MAGI cannot inflate the top-up # above the benchmark - APTC residual via the -pct*income term. income = max_(tax_unit("aca_magi", period), 0) - # slcsp is a MONTH-period variable; core sums the 12 months when - # called from this YEAR formula. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, a MONTH variable that + # core sums over the year here: both components cover only enrollees + # who meet the federal PTC criteria (the base program in full, the + # Middle Income Household component except for income). + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) magi_frac = tax_unit("aca_magi_fraction", period) diff --git a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py index 78a6a44ff95..bbb6133f84d 100644 --- a/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py +++ b/policyengine_us/variables/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py @@ -29,7 +29,11 @@ def formula(tax_unit, period, parameters): # the Medicaid coverage gap, income under the federal PTC floor of 100% # FPL; that cannot bind here, because income_eligible already requires # income above 400% FPL. - pays_premium = add(tax_unit, period, ["pays_aca_premium"]) > 0 + # The Addendum requires every federal PTC criterion except income, so + # the enrollee must also be in the federal tax family (26 U.S.C. + # 36B(c)(1)(D)): is_aca_coverage_family_member is pays_aca_premium + # restricted to that family. + pays_premium = add(tax_unit, period, ["is_aca_coverage_family_member"]) > 0 filing_status = tax_unit("filing_status", period) not_separate = filing_status != filing_status.possible_values.SEPARATE # is_aca_ptc_eligible does not embed the federal PTC filing requirement diff --git a/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py b/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py index b19a3450ccd..ff3d842334d 100644 --- a/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py +++ b/policyengine_us/variables/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.py @@ -19,9 +19,9 @@ class vt_premium_assistance(Variable): "percentage-point reduction to the federal section 36B applicable " "percentage, so the subsidy equals the enrollee's income times that " "reduction, capped at the benchmark premium remaining after the federal " - "APTC so it never duplicates the federal credit. The benchmark second-" - "lowest-cost silver plan premium (slcsp) is annualized from its monthly " - "definition. At very low income the applicable percentage can fall below " + "APTC so it never duplicates the federal credit. The premium tax " + "credit's benchmark for the coverage family (aca_ptc_slcsp) is " + "annualized from its monthly definition. At very low income the applicable percentage can fall below " "1.5%, in which case the reduction floors at zero and the cap lets " "Vermont Premium Assistance cover the full residual premium after the " "federal APTC. The amount is not taxable. Advance-payment, " @@ -54,8 +54,10 @@ def formula(tax_unit, period, parameters): # Incremental contribution gap between the federal applicable # percentage and the lower Vermont percentage. contribution_gap = max_(0, income * (federal_percentage - vt_percentage)) - # slcsp is a MONTH-period variable; annualize it to match aca_ptc. - slcsp = add(tax_unit, period, ["slcsp"]) + # The benchmark for the federal coverage family, annualized to match + # aca_ptc: 33 V.S.A. 1812(a)(1) covers only those eligible for the + # federal premium tax credit. + slcsp = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) # Cap at the premium balance remaining after the federal APTC. premium_after_aptc = max_(0, slcsp - aca_ptc) diff --git a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py index 307e9cc6f90..de701b06234 100644 --- a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py +++ b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.py @@ -10,6 +10,7 @@ class wa_cascade_care_savings_member_amount(Variable): defined_for = StateCode.WA reference = ( "https://app.leg.wa.gov/rcw/default.aspx?cite=43.71.110", + # PDF pages 1, 5 (the PMPM amounts and the three groups). "https://www.wahbexchange.org/content/dam/materials/communications/legislative/2025/WAHBE_Final_PY_2026_Cascade_Care_Savings_Maximum_Per_Member_Per_Month_Methodology.pdf#page=1", "https://www.wahbexchange.org/content/dam/wahbe-assets/materials/collateral/cc/FinalPY2026CascadeCareSavingsPolicy_Combined.pdf#page=14", ) @@ -22,15 +23,18 @@ class wa_cascade_care_savings_member_amount(Variable): "with-federal-subsidy amount ($55 PMPM for PY2026), and Group 3 " "members without a federally recognized immigration status who lack " "minimum essential coverage through a state medical assistance program " - "receive the without-federal-subsidy amount ($250 PMPM for PY2026). " + "receive the without-federal-subsidy amount ($250 PMPM for PY2026), " + "as do Group 2 members modeled as QHP-eligible enrollees outside the " + "federal tax family (someone another taxpayer can claim). " "Group 3 excludes members eligible for Washington Apple Health " "Expansion (undocumented adults) or Apple Health for Kids (children " "under 19), per Policy Section 4(1)(f), which also avoids double " "counting with the Medicaid-cost proxy in healthcare_benefit_value; it " "further requires the member not be premium-tax-credit-eligible, so " - "Groups 1 and 3 are mutually exclusive. Group 2 (lawfully present but " - "premium-tax-credit-ineligible) is not separately identifiable in the " - "model and is documented away. The monthly amount is annualized by " + "Groups 1 and 3 are mutually exclusive. Other Group 2 members " + "(lawfully present but premium-tax-credit-ineligible for other " + "reasons) are not separately identifiable in the model and are " + "documented away. The monthly amount is annualized by " "MONTHS_IN_YEAR, assuming full-year enrollment." ) @@ -54,6 +58,11 @@ def formula(person, period, parameters): apple_health_expansion = person("wa_apple_health_expansion_eligible", period) apple_health_kids = person("wa_apple_health_kids_eligible", period) group_3 = undocumented & ~apple_health_expansion & ~apple_health_kids & ~group_1 + # Group 2: QHP-eligible enrollees outside the federal tax family, who + # have no federal subsidy (see wa_cascade_care_savings_member_eligible). + group_2 = person("pays_aca_premium", period) & ~person( + "is_aca_tax_family_member", period + ) with_subsidy = where(group_1, p.amount.with_federal_subsidy, 0) - without_subsidy = where(group_3, p.amount.without_federal_subsidy, 0) + without_subsidy = where(group_2 | group_3, p.amount.without_federal_subsidy, 0) return MONTHS_IN_YEAR * (with_subsidy + without_subsidy) diff --git a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py index 1eb0c88c9db..d931c332c49 100644 --- a/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py +++ b/policyengine_us/variables/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.py @@ -13,10 +13,14 @@ class wa_cascade_care_savings_member_eligible(Variable): "https://www.wahbexchange.org/content/dam/materials/communications/legislative/2025/WAHBE_Final_PY_2026_Cascade_Care_Savings_Maximum_Per_Member_Per_Month_Methodology.pdf#page=5", ) documentation = ( - "A person is a Cascade Care Savings enrollee if they fall in Group 1 " - "or Group 3. Group 1 members are eligible for the federal ACA premium " + "A person is a Cascade Care Savings enrollee if they fall in Group 1, " + "2 or 3. Group 1 members are eligible for the federal ACA premium " "tax credit (embedding on-Marketplace enrollment, the MFS exclusion, " "immigration/TIN status, and the required-contribution income test). " + "Group 2 members are QHP-eligible but not eligible for the federal " + "credit; the model identifies those outside the federal tax family " + "(someone another taxpayer can claim, or a dependent of a return with " + "such a filer), since Policy Section 4(2) does not exclude them. " "Group 3 members are undocumented residents who lack minimum essential " "coverage through a state medical assistance program: they are excluded " "if eligible for Washington Apple Health Expansion (undocumented adults) " @@ -24,8 +28,9 @@ class wa_cascade_care_savings_member_eligible(Variable): "4(1)(f), which also avoids double counting with the Medicaid-cost " "proxy in healthcare_benefit_value. Group 3 further requires the member " "not be premium-tax-credit-eligible, making Groups 1 and 3 mutually " - "exclusive. Group 2 (lawfully present but premium-tax-credit-" - "ineligible) is not separately identifiable and is documented away." + "exclusive. Other Group 2 members (lawfully present but premium-tax-" + "credit-ineligible for other reasons) are not separately identifiable " + "and are documented away." ) def formula(person, period, parameters): @@ -43,4 +48,13 @@ def formula(person, period, parameters): apple_health_expansion = person("wa_apple_health_expansion_eligible", period) apple_health_kids = person("wa_apple_health_kids_eligible", period) group_3 = undocumented & ~apple_health_expansion & ~apple_health_kids & ~group_1 - return group_1 | group_3 + # Group 2: QHP-eligible enrollees outside the federal tax family. The + # PMPM methodology defines Group 2 as QHP-eligible residents "not + # eligible for APTC Federal subsidies but eligible for CCS", and + # Policy Section 4(2) does not list being claimable among its + # exclusions. pays_aca_premium already excludes undocumented people, + # so Groups 2 and 3 do not overlap. + group_2 = person("pays_aca_premium", period) & ~person( + "is_aca_tax_family_member", period + ) + return group_1 | group_2 | group_3 From ec0aaef87d6b496c9d7079869793b41c83047017 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 12:36:17 -0400 Subject: [PATCH 3/7] Test the premium tax credit invariants for claimable filers Co-Authored-By: Claude Opus 5.5 --- .../test_claimable_filer_ptc_invariants.py | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py new file mode 100644 index 00000000000..c0ddc4c9fc2 --- /dev/null +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -0,0 +1,224 @@ +"""The premium tax credit for a filer who can be claimed as a dependent. + +Someone another taxpayer can claim is not an applicable taxpayer (IRC +36B(c)(1)(D)) and is outside the tax family (26 CFR 1.36B-1(d)(2)); a return +with such a filer claims no dependents (IRC 152(b)(1)). The credit therefore +uses `aca_tax_family_size` for the poverty line and the coverage family's +benchmark `aca_ptc_slcsp`, and the state premium programs that wrap it follow. + +For couples drawn by Hypothesis and a seeded population, with either, both or +neither spouse claimed: + +1. Swap invariance: each output is the same under either head/spouse + labelling. +2. Identities: `aca_tax_family_size` equals the filers who cannot be claimed + plus the dependents when no filer can be claimed; nobody outside the tax + family is eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; a return where every + filer can be claimed has no credit; and a return where no filer can be + claimed keeps `slcsp`, the tax unit size and the tax unit's poverty line. +3. Monotonicity: marking another filer as claimed never raises the tax family + size, never raises the benchmark while the enrollees stay the same, and + never raises the credit of a return that had an eligible member. Both can + rise otherwise, as intended: a couple below 100% of the two-person line can + be above it for a tax family of one, so IRC 36B allows the credit and the + unclaimable spouse leaves the coverage gap that pays_aca_premium assumes + nobody buys into. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.tests.core.test_dependent_elsewhere_head_spouse_swap_invariance import ( + CLAIM_PATTERNS, + TOLERANCE, + _calc, + _crafted_cases, + _monotone_variants, + _seeded_couples, + _situation, + _swap_variants, + adults, + dependents, +) + +# Texas (age curve, no expansion), New York and Vermont (family tiers), and +# the states whose premium programs wrap the credit. +STATES = ["TX", "NY", "VT", "MA", "CT", "CO", "MD", "CA", "NM", "NJ", "WA"] +YEARS = [2021, 2022, 2023, 2024, 2025, 2026] +FEDERAL = [ + "aca_tax_family_size", + "aca_fpg", + "aca_magi_fraction", + "slcsp", + "aca_ptc_slcsp", + "aca_ptc", + "marketplace_csr_eligible", +] +STATE_WRAPS = [ + "ma_connector_care", + "ct_covered_connecticut", + "co_premium_assistance", + "md_premium_assistance", + "ca_premium_subsidy", + "nm_premium_assistance", + "vt_premium_assistance", + "nj_njhps", + "wa_cascade_care_savings", +] +OUTPUTS = FEDERAL + STATE_WRAPS + + +@st.composite +def couples(draw): + return { + "state": draw(st.sampled_from(STATES)), + "adults": [draw(adults()), draw(adults())], + "dependents": draw(st.lists(dependents(), max_size=2)), + "claimed": draw(st.sampled_from(CLAIM_PATTERNS)), + } + + +def _ptc_cases(states): + # Two 30-year-olds whose income is 150% of the one-person line in 2025, + # with and without a child: the credit for the unclaimable spouse. + adult = {"age": 30, "employment_income": 11_295.0} + cases = [] + for state in states: + for claimed in [(True, False), (True, True)]: + cases += [ + { + "state": state, + "adults": [adult, adult], + "dependents": [], + "claimed": claimed, + }, + { + "state": state, + "adults": [adult, adult], + "dependents": [{"age": 4}], + "claimed": claimed, + }, + ] + return cases + + +def _tax_unit_sum(sim, name, year): + return np.asarray(sim.calculate(name, year, map_to="tax_unit"), dtype=float) + + +def _identities(sim, year): + claimed_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + independent = _calc(sim, "head_spouse_count_not_dependent_elsewhere", year) + dependents_count = _calc(sim, "tax_unit_count_dependents", year) + size = _calc(sim, "aca_tax_family_size", year) + np.testing.assert_array_equal( + size, independent + np.where(claimed_filer, 0, dependents_count) + ) + eligible = np.asarray(sim.calculate("is_aca_ptc_eligible", year)) + member = np.asarray(sim.calculate("is_aca_tax_family_member", year)) + assert not (eligible & ~member).any(), "an eligible person is outside the family" + slcsp = _calc(sim, "slcsp", year) + benchmark = _calc(sim, "aca_ptc_slcsp", year) + assert (benchmark >= -TOLERANCE).all() + assert (benchmark <= slcsp + TOLERANCE).all(), "benchmark above slcsp" + nobody_in_family = size == 0 + assert not _calc(sim, "aca_ptc", year)[nobody_in_family].any() + unclaimed = ~claimed_filer + np.testing.assert_allclose(benchmark[unclaimed], slcsp[unclaimed], atol=TOLERANCE) + np.testing.assert_array_equal( + size[unclaimed], _calc(sim, "tax_unit_size", year)[unclaimed] + ) + np.testing.assert_allclose( + _calc(sim, "aca_fpg", year)[unclaimed], + _calc(sim, "tax_unit_fpg", year - 1)[unclaimed], + ) + + +def check_swap(units, year, outputs, require_coverage=False): + sim = Simulation(situation=_situation(units, year, _swap_variants)) + if require_coverage: + # The batch must reach the rule: returns with exactly one claimable + # filer that still get a credit and, from 2026 when the modeled state + # programs start, a state wrap paid on one. + one_claimed = ( + _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + ) & (_calc(sim, "head_spouse_count_not_dependent_elsewhere", year) == 1) + assert (_calc(sim, "aca_ptc", year)[one_claimed] > 0).any() + if year >= 2026: + wraps = sum(_calc(sim, name, year) for name in STATE_WRAPS) + assert (wraps[one_claimed] > 0).any() + values = {name: _calc(sim, name, year) for name in outputs} + values["eligible members"] = _tax_unit_sum(sim, "is_aca_ptc_eligible", year) + for name, value in values.items(): + assert np.isfinite(value).all(), f"{name} in {year} is not finite" + drawn, swapped = value[0::2], value[1::2] + differs = ~(np.abs(swapped - drawn) <= TOLERANCE) + assert not differs.any(), ( + f"{name} in {year} changes when the head and spouse labels are " + "exchanged: " + + ", ".join( + f"{units[i]['state']} claimed={units[i]['claimed']} " + f"{drawn[i]:.2f} -> {swapped[i]:.2f}" + for i in np.flatnonzero(differs)[:10] + ) + ) + _identities(sim, year) + + +def check_monotone(units, year): + sim = Simulation(situation=_situation(units, year, _monotone_variants)) + had_eligible = (_tax_unit_sum(sim, "is_aca_ptc_eligible", year) > 0).reshape(-1, 4) + enrollees = _tax_unit_sum(sim, "pays_aca_premium", year).reshape(-1, 4) + for name, guard in ( + ("aca_tax_family_size", None), + ("aca_ptc_slcsp", "same enrollees"), + ("aca_ptc", "had eligible"), + ): + values = _calc(sim, name, year).reshape(-1, 4) + nobody, first, second, both = values.T + for more, fewer, base, label in ( + (first, nobody, 0, "first adult"), + (second, nobody, 0, "second adult"), + (both, first, 1, "both after first"), + (both, second, 2, "both after second"), + ): + rises = more > fewer + TOLERANCE + if guard == "had eligible": + rises &= had_eligible[:, base] + elif guard == "same enrollees": + more_index = {"first adult": 1, "second adult": 2}.get(label, 3) + rises &= enrollees[:, base] == enrollees[:, more_index] + assert not rises.any(), ( + f"{name} in {year} rises when the {label} is claimed " + "elsewhere: " + + ", ".join( + f"{units[i]['state']} {fewer[i]:.2f} -> {more[i]:.2f}" + for i in np.flatnonzero(rises)[:10] + ) + ) + _identities(sim, year) + + +@settings( + max_examples=4, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=15)) +def test_claimable_filer_ptc_is_label_free(year, units): + check_swap(units, year, OUTPUTS) + + +@pytest.mark.parametrize("year", [2025, 2026]) +def test_claimable_filer_ptc_seeded_population(year): + units = ( + _seeded_couples(STATES, per_state=2) + + _crafted_cases(STATES, review_case_only=True) + + _ptc_cases(STATES) + ) + check_swap(units, year, OUTPUTS, require_coverage=True) + check_monotone(units, year) From 948f6bed236abde7fb8d65f73e56ef2a96822f46 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 05:45:13 -0400 Subject: [PATCH 4/7] Cite 36B(c)(1)(D) and 1.36B-2(b)(3) for what each says Co-Authored-By: Claude Opus 5.5 --- .../variables/gov/aca/eligibility/is_aca_ptc_eligible.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py index 34e4cc60e67..88cdd68742d 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py @@ -24,9 +24,10 @@ def formula(person, period, parameters): ) is_income_eligible = standard_income_eligible | below_fpl_exception - # Someone another taxpayer can claim is not an applicable taxpayer - # (26 U.S.C. 36B(c)(1)(D)) and is outside the tax family, along with - # the dependents of a return that has such a filer. + # No credit is allowed to someone another taxpayer can claim (26 + # U.S.C. 36B(c)(1)(D)); they are not an applicable taxpayer (26 CFR + # 1.36B-2(b)(3)) and are outside the tax family, along with the + # dependents of a return that has such a filer (IRC 152(b)(1)). tax_family_member = person("is_aca_tax_family_member", period) return ( From dc66236d6f7020945a9c0574928aedeb76fc35ea Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:29:46 -0400 Subject: [PATCH 5/7] Address review r1: claimability, household income, premium cap, CSR, role-less units - is_aca_tax_family_member reads claimable_as_dependent_on_another_return (#10024's split input: 36B(c)(1)(D) and 1.36B-1(d)(2) turn on being claimable, and Form 1040 line 12a, which Form 8962 line 1 keys on, has no refund-only carve-out). The return keeps its dependents when the would-be claimant meets the refund-only filing exception (Pub 501, Dependent Taxpayer Test), a return with no applicable taxpayer has no family, and a tax unit with neither a head nor a spouse uses each member's own claimability (a lone claimable minor no longer gets the credit). - aca_magi leaves out the tax-exempt interest and Social Security of the return's dependents who are outside the tax family (26 CFR 1.36B-1(e)(1)). - used_aca_ptc is capped at the coverage family's share of the selected plan premium (new aca_ptc_coverage_family_premium; 26 CFR 1.36B-3(d)(1)(i), (h)(1)), not the whole policy's premium. - marketplace_csr_eligible applies 45 CFR 155.305(g)(3): the model prices one policy per tax unit, and a policy that also covers an enrollee without credit eligibility gets no cost-sharing reductions. - aca_fpg documents that it sizes the family for the tax year; the NJHPS exclusion is labelled an interpretation; one reference per source in is_aca_coverage_family_member. Co-Authored-By: Claude Opus 5.5 --- .../baseline/gov/aca/eligibility/aca_fpg.yaml | 14 +++--- .../aca/eligibility/aca_magi_fraction.yaml | 6 +-- ...a_ptc_below_fpl_immigration_exception.yaml | 4 +- .../aca/eligibility/aca_tax_family_size.yaml | 10 ++--- .../is_aca_coverage_family_member.yaml | 4 +- .../aca/eligibility/is_aca_ptc_eligible.yaml | 18 ++++---- .../eligibility/is_aca_tax_family_member.yaml | 14 +++--- .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 24 +++++------ .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 18 ++++---- .../aca_ptc_slcsp_family_tier_category.yaml | 16 +++---- .../aca_ptc_slcsp_family_tier_multiplier.yaml | 16 +++---- .../premium_subsidy/ca_premium_subsidy.yaml | 4 +- .../co_premium_assistance.yaml | 8 ++-- .../ct_covered_connecticut.yaml | 8 ++-- .../hic/connector_care/ma_connector_care.yaml | 8 ++-- .../ma_connector_care_member_eligible.yaml | 4 +- .../md_premium_assistance.yaml | 8 ++-- .../gov/states/nj/dobi/njhps/nj_njhps.yaml | 8 ++-- .../dobi/njhps/nj_njhps_member_eligible.yaml | 4 +- .../nm_premium_assistance.yaml | 8 ++-- .../nm_premium_assistance_mih_eligible.yaml | 8 ++-- .../vt_premium_assistance.yaml | 8 ++-- .../wa_cascade_care_savings.yaml | 8 ++-- ...wa_cascade_care_savings_member_amount.yaml | 4 +- ..._cascade_care_savings_member_eligible.yaml | 4 +- .../policy/contrib/aca/ptc_700_fpl_cliff.yaml | 4 +- .../contrib/aca/ptc_additional_bracket.yaml | 4 +- .../contrib/aca/ptc_simplified_bracket.yaml | 4 +- .../gov/aca/csr/marketplace_csr_eligible.py | 17 +++++++- .../variables/gov/aca/eligibility/aca_fpg.py | 14 +++--- .../variables/gov/aca/eligibility/aca_magi.py | 32 +++++++++++++- .../aca/eligibility/aca_tax_family_size.py | 3 +- .../is_aca_coverage_family_member.py | 5 +-- .../eligibility/is_aca_tax_family_member.py | 43 ++++++++++++------- .../ptc/aca_ptc_coverage_family_premium.py | 33 ++++++++++++++ .../variables/gov/aca/ptc/used_aca_ptc.py | 6 ++- .../gov/states/nj/dobi/njhps/nj_njhps.py | 3 +- .../nj/dobi/njhps/nj_njhps_member_eligible.py | 15 ++++--- 38 files changed, 258 insertions(+), 161 deletions(-) create mode 100644 policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml index 9d31224ee0b..8f2df037394 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -56,7 +56,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -125,7 +125,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a] @@ -149,7 +149,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -158,7 +158,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -183,7 +183,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -223,7 +223,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml index e352b8a0826..5f8e0b0d996 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi_fraction.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -50,7 +50,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -109,7 +109,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml index fd777a8eb45..8e6b856b402 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_below_fpl_immigration_exception.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true immigration_status: TPS is_basic_health_program_eligible: false is_tax_unit_head: true @@ -53,7 +53,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true immigration_status: TPS is_basic_health_program_eligible: false is_tax_unit_head: false diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml index 636b3ff9473..3133369a77d 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true child: age: 10 is_medicaid_eligible: false @@ -40,7 +40,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -96,7 +96,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -133,7 +133,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -142,7 +142,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml index 6064d5ac59d..cd17edc030b 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_coverage_family_member.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -57,7 +57,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml index 7d03af20362..519ed8c4040 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_ptc_eligible.yaml @@ -506,7 +506,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -550,7 +550,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -576,7 +576,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a] @@ -599,7 +599,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -608,7 +608,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -665,7 +665,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -713,7 +713,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -745,7 +745,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -790,7 +790,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml index 7678861083c..7a150cdf659 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true child: age: 10 is_medicaid_eligible: false @@ -39,7 +39,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -88,7 +88,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -118,7 +118,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -127,7 +127,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -184,7 +184,7 @@ output: is_aca_tax_family_member: [true, true, true] -- name: 2025 dependent flagged as claimed elsewhere stays in the family of the return that claims them +- name: 2025 dependent another taxpayer could also claim stays in the family of the return that claims them period: 2025 input: people: @@ -196,7 +196,7 @@ is_medicare_eligible: false child: age: 10 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true tax_units: tax_unit: members: [person_a, child] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 49a6222cef5..462efa51568 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -268,7 +268,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -317,7 +317,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -346,7 +346,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 tax_units: tax_unit: @@ -372,7 +372,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -382,7 +382,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -448,7 +448,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -497,7 +497,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -526,7 +526,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 tax_units: tax_unit: @@ -552,7 +552,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -562,7 +562,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -628,7 +628,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -675,7 +675,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml index 5720af34beb..51ae163bac9 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -8,7 +8,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -62,7 +62,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -138,7 +138,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 tax_units: tax_unit: @@ -163,7 +163,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: true is_tax_unit_spouse: false @@ -173,7 +173,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true slcsp_age_curve_amount_person: 6000 is_tax_unit_head: false is_tax_unit_spouse: true @@ -232,7 +232,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -275,7 +275,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -306,7 +306,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -349,7 +349,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml index 80cf923afc6..a2269f7968d 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_category.yaml @@ -6,7 +6,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -48,7 +48,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -78,14 +78,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -149,7 +149,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -191,7 +191,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -221,14 +221,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml index e175f8f9941..427bf1091c7 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp_family_tier_multiplier.yaml @@ -6,7 +6,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -48,7 +48,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -78,14 +78,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -114,7 +114,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -156,7 +156,7 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: @@ -186,14 +186,14 @@ age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: age: 30 monthly_age: 30 pays_aca_premium: true - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true child: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 34561143831..78d17264213 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -518,7 +518,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -570,7 +570,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml index fdd1995fa79..0df47cd45e4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml @@ -695,7 +695,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -748,7 +748,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -816,7 +816,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -827,7 +827,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml index 22c48b87423..3fd48f2227c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/covered_connecticut/ct_covered_connecticut.yaml @@ -385,7 +385,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -438,7 +438,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -506,7 +506,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -517,7 +517,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml index b52405c1e14..b254072224a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care.yaml @@ -580,7 +580,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -633,7 +633,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -701,7 +701,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -712,7 +712,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml index 27c4ffd05fa..80802d19164 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/hic/connector_care/ma_connector_care_member_eligible.yaml @@ -70,7 +70,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -118,7 +118,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml index fe6090f2e5b..6ab68a2a44b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/mhbe/premium_assistance/md_premium_assistance.yaml @@ -737,7 +737,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -790,7 +790,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -858,7 +858,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -869,7 +869,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml index 9982d762b4a..dda7665f27a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps.yaml @@ -806,7 +806,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -859,7 +859,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -927,7 +927,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -938,7 +938,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml index a17d1e77e50..095bbeaa877 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.yaml @@ -280,7 +280,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -328,7 +328,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml index 3b634cb0983..a0238c6b2ba 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance.yaml @@ -899,7 +899,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -952,7 +952,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -1020,7 +1020,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -1031,7 +1031,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml index 4a86773a91f..60f13561961 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.yaml @@ -251,7 +251,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -303,7 +303,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -333,7 +333,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -344,7 +344,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml index 0623d929169..1ae052648d3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/dvha/premium_assistance/vt_premium_assistance.yaml @@ -419,7 +419,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -470,7 +470,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -537,7 +537,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -547,7 +547,7 @@ is_aca_eshi_eligible: false is_medicare_eligible: false is_basic_health_program_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml index 5d2dd12d03d..062ef7cd7cd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings.yaml @@ -763,7 +763,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -817,7 +817,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: @@ -885,7 +885,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -896,7 +896,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml index 2caf6ea14e0..73694ff8b65 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_amount.yaml @@ -167,7 +167,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -216,7 +216,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml index e6343dfebfc..c18970e2535 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/wahbe/cascade_care_savings/wa_cascade_care_savings_member_eligible.yaml @@ -186,7 +186,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -234,7 +234,7 @@ is_medicare_eligible: false is_basic_health_program_eligible: false slcsp_age_curve_amount_person: 6000 - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml index 737cdc45428..11dd41d0214 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_700_fpl_cliff.yaml @@ -311,7 +311,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -356,7 +356,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml index ce8da297e6e..8c568ee903e 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_additional_bracket.yaml @@ -128,7 +128,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -173,7 +173,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml index 820a2cb1b42..cd3d6932461 100644 --- a/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml +++ b/policyengine_us/tests/policy/contrib/aca/ptc_simplified_bracket.yaml @@ -128,7 +128,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: true is_tax_unit_spouse: false person_b: @@ -173,7 +173,7 @@ is_chip_eligible: false is_aca_eshi_eligible: false is_medicare_eligible: false - claimed_as_dependent_on_another_return: true + claimable_as_dependent_on_another_return: true is_tax_unit_head: false is_tax_unit_spouse: true tax_units: diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py index 837491802ee..5d6718c052a 100644 --- a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py +++ b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py @@ -6,15 +6,28 @@ class marketplace_csr_eligible(Variable): entity = TaxUnit label = "Tax unit is eligible for ordinary Marketplace cost-sharing reductions" definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/45/155.305#g_1" + reference = "https://www.law.cornell.edu/cfr/text/45/155.305#g" + documentation = ( + "The model prices one Marketplace policy per tax unit. Under 45 CFR " + "155.305(g)(3), a policy covering people who would be eligible for " + "different cost-sharing reductions gets only the category all of " + "them qualify for, so a policy that also covers an enrollee without " + "premium tax credit eligibility (one outside the tax family, such as " + "a spouse who can be claimed on another return) gets none." + ) def formula(tax_unit, period, parameters): person = tax_unit.members receives_aca = tax_unit.sum(person("person_receives_aca", period)) > 0 + enrollee = person("pays_aca_premium", period) + ptc_eligible = person("is_aca_ptc_eligible", period) + every_enrollee_eligible = ~tax_unit.any(enrollee & ~ptc_eligible) category = tax_unit("selected_marketplace_plan_category", period) silver_selected = category == category.possible_values.SILVER magi_fraction = tax_unit("aca_magi_fraction", period) income_eligible = ( magi_fraction <= parameters(period).gov.aca.csr.income_threshold.maximum ) - return receives_aca & silver_selected & income_eligible + return ( + receives_aca & every_enrollee_eligible & silver_selected & income_eligible + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py index 2a882f1a066..0ba7f9a9adc 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py @@ -15,11 +15,15 @@ class aca_fpg(Variable): ) documentation = ( "Form 8962 line 4: the prior year's poverty guideline for the tax " - "family size on line 1 (aca_tax_family_size). A return on which " - "every head and spouse can be claimed elsewhere has no tax family and " - "no applicable taxpayer; it keeps the guideline for the whole tax " - "unit, so readers that are not about the credit see the same value " - "as before." + "year's tax family size on line 1 (aca_tax_family_size). The former " + "lookup, tax_unit_fpg for the prior year, also took the household " + "size from the prior year; the size now comes from the tax year, as " + "line 1 requires, which differs only when a prior-year size is " + "supplied. A return on which every head and spouse can be claimed " + "elsewhere has no tax family and no applicable taxpayer; as a " + "numerical convention, not a legal rule, it keeps the guideline for " + "the whole tax unit, so readers that are not about the credit see " + "the same value as before." ) def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py index 2b9764bf6af..bdd10aa10ac 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py @@ -6,4 +6,34 @@ class aca_magi(Variable): entity = TaxUnit label = "ACA-related modified AGI for this tax unit" definition_period = YEAR - adds = ["medicaid_magi"] + unit = USD + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#d_2", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#e_1", + ) + documentation = ( + "Household income for the premium tax credit (IRC 36B(d)(2), 26 CFR " + "1.36B-1(e)(1)): the taxpayer's modified AGI, both spouses' on a " + "joint return, plus that of other members of the tax family. It is " + "medicaid_magi without the tax-exempt interest and Social Security " + "of the return's dependents who are outside the tax family " + "(is_aca_tax_family_member), which happens only on a return whose " + "head or spouse can be claimed elsewhere. medicaid_magi adds those " + "two amounts for every member (tax unit AGI already leaves " + "dependents' income out); both are nonnegative, so removing them " + "after medicaid_magi's zero floor gives the same result as removing " + "them before it." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + filer = person("is_tax_unit_head_or_spouse", period) + outside_family = ~filer & ~person("is_aca_tax_family_member", period) + excluded = tax_unit.sum( + outside_family + * ( + person("tax_exempt_interest_income", period) + + person("social_security", period) + ) + ) + return max_(0, tax_unit("medicaid_magi", period) - excluded) diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py index d8cea21ac2d..96889d8b9ec 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_tax_family_size.py @@ -15,7 +15,8 @@ class aca_tax_family_size(Variable): "Form 8962 line 1: the number of people in the premium tax credit " "tax family. It equals the tax unit size unless a head or spouse can " "be claimed as a dependent on another return, and is 0 when every " - "head and spouse can be. The shared tax_unit_size is unchanged." + "head and spouse can be (no applicable taxpayer). The shared " + "tax_unit_size is unchanged." ) adds = ["is_aca_tax_family_member"] diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py index 022482995d0..389b06a6a8c 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py @@ -6,10 +6,7 @@ class is_aca_coverage_family_member(Variable): entity = Person label = "Member of the premium tax credit coverage family" definition_period = YEAR - reference = ( - "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2", - "https://www.law.cornell.edu/cfr/text/26/1.36B-3#f_1", - ) + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2" documentation = ( "A Marketplace enrollee (pays_aca_premium) who is in the premium tax " "credit tax family. 26 CFR 1.36B-3(b)(2) defines the coverage family " diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py index ea43bed46d3..cac4db03b6c 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -12,26 +12,37 @@ class is_aca_tax_family_member(Variable): "https://www.law.cornell.edu/cfr/text/26/1.36B-1#d_2", "https://www.law.cornell.edu/cfr/text/26/1.36B-2#b_3", "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=11", ) documentation = ( - "Whether the person is in the taxpayer's family for the premium tax " - "credit (Form 8962 line 1, tax family size). 26 CFR 1.36B-1(d)(2) " - "defines the family as the taxpayer, both spouses on a joint return, " - "'except for individuals who qualify as a dependent of another " - "taxpayer', plus the dependents the taxpayer claims. A head or spouse " - "who can be claimed on another return is therefore not a member " - "(IRC 36B(c)(1)(D), 26 CFR 1.36B-2(b)(3)), and on a joint return the " - "other spouse still is. A return with a filer who can be claimed " - "elsewhere claims no dependents (IRC 152(b)(1)), so its dependents " - "are members only when no head or spouse can be claimed elsewhere. " - "The claimed flag of a dependent on this return does not matter: this " - "return claims them." + "Whether the person is in an applicable taxpayer's family for the " + "premium tax credit (Form 8962 line 1, tax family size). 26 CFR " + "1.36B-1(d)(2) defines the family as the taxpayer, both spouses on a " + "joint return, 'except for individuals who qualify as a dependent of " + "another taxpayer', plus the dependents the taxpayer claims. A head or " + "spouse who can be claimed on another return " + "(claimable_as_dependent_on_another_return, whether or not claimed) " + "is not a member; on a joint return the other spouse still is. Such a " + "return claims no dependents (IRC 152(b)(1)) unless the would-be " + "claimant meets the refund-only filing exception (Publication 501, " + "Dependent Taxpayer Test). No credit is allowed to someone another " + "taxpayer can claim (IRC 36B(c)(1)(D)), so a return whose every head " + "and spouse can be claimed has no applicable taxpayer and no family " + "for the credit. A tax unit with neither a head nor a spouse has no " + "modeled filer, so each member's own claimability decides. A " + "dependent on a return with an applicable taxpayer is a member even if " + "another taxpayer could also claim them: this return claims them." ) def formula(person, period, parameters): filer = person("is_tax_unit_head_or_spouse", period) - claimed_elsewhere = person("claimed_as_dependent_on_another_return", period) - return_has_claimed_filer = person.tax_unit( - "head_or_spouse_is_dependent_elsewhere", period + claimable = person("claimable_as_dependent_on_another_return", period) + has_filer = person.tax_unit.any(filer) + applicable_taxpayer = ( + person.tax_unit("head_spouse_count_not_dependent_elsewhere", period) > 0 ) - return where(filer, ~claimed_elsewhere, ~return_has_claimed_filer) + claims_dependents = ~person.tax_unit( + "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period + ) + dependent_member = applicable_taxpayer & claims_dependents + return where(has_filer, where(filer, ~claimable, dependent_member), ~claimable) diff --git a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py new file mode 100644 index 00000000000..d06c3482cf2 --- /dev/null +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class aca_ptc_coverage_family_premium(Variable): + value_type = float + entity = TaxUnit + label = "Selected Marketplace plan premium allocated to the coverage family" + unit = USD + definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#d_1" + documentation = ( + "The enrollment premiums for the premium tax credit's lesser-of test " + "(26 CFR 1.36B-3(d)(1)(i)): the selected plan premium for the tax " + "unit, of which the coverage family's share is its benchmark " + "(aca_ptc_slcsp) over the whole enrollment's benchmark (slcsp). 26 " + "CFR 1.36B-3(h)(1) allocates a policy covering more than one family " + "'in proportion to the premiums for each taxpayer's applicable " + "benchmark plan'; the whole-enrollment benchmark stands in for the " + "sum of those benchmarks. The share is 1 whenever every enrollee is " + "in the coverage family. It applies to bronze plans too." + ) + + def formula(tax_unit, period, parameters): + premium = tax_unit("selected_marketplace_plan_premium_proxy", period) + benchmark = add(tax_unit, period, ["slcsp"]) + coverage_family_benchmark = add(tax_unit, period, ["aca_ptc_slcsp"]) + share = np.divide( + coverage_family_benchmark, + benchmark, + out=np.ones_like(benchmark), + where=benchmark > 0, + ) + return premium * share diff --git a/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py b/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py index 6de35a53b29..da994b651b4 100644 --- a/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py +++ b/policyengine_us/variables/gov/aca/ptc/used_aca_ptc.py @@ -7,9 +7,13 @@ class used_aca_ptc(Variable): label = "Used ACA premium tax credit" unit = USD definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#d_1" def formula(tax_unit, period, parameters): + # The credit cannot exceed the enrollment premiums of the coverage + # family (26 CFR 1.36B-3(d)(1)(i)); an enrollee outside the tax family + # pays their own share of the policy. return min_( tax_unit("aca_ptc", period), - tax_unit("selected_marketplace_plan_premium_proxy", period), + tax_unit("aca_ptc_coverage_family_premium", period), ) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py index 97c660c1ad8..14c442961c6 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps.py @@ -50,7 +50,8 @@ def formula(tax_unit, period, parameters): # premium below zero, so it is capped at the post-APTC residual. In the # 400-600% band aca_ptc is 0, so the residual equals slcsp_annual. # The benchmark is the federal coverage family's: NJHPS members must - # be in a tax household otherwise eligible for APTC. + # be in a tax household otherwise eligible for APTC (an + # interpretation; see nj_njhps_member_eligible). slcsp_annual = add(tax_unit, period, ["aca_ptc_slcsp"]) aca_ptc = tax_unit("aca_ptc", period) residual = max_(0, slcsp_annual - aca_ptc) diff --git a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py index f46e8605147..2578759eebe 100644 --- a/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py +++ b/policyengine_us/variables/gov/states/nj/dobi/njhps/nj_njhps_member_eligible.py @@ -14,8 +14,10 @@ class nj_njhps_member_eligible(Variable): documentation = ( "A person is a New Jersey Health Plan Savings enrollee when they pay an " "ACA marketplace premium, are in the federal premium tax credit tax " - "family (NJHPS goes to enrollees otherwise eligible for APTC, so not " - "someone another taxpayer can claim), do not file a separate return, " + "family (an interpretation: Treasury's 1332 addendum describes NJHPS " + "as for enrollees 'otherwise eligible for the Advanced Premium Tax " + "Credit', and no current New Jersey rule found addresses someone " + "another taxpayer can claim), do not file a separate return, " "and household " "income (ACA MAGI per 26 U.S.C. 36B(d)(2)) falls within the 138-600% " "FPL NJHPS band. This deliberately does NOT reuse is_aca_ptc_eligible: " @@ -48,10 +50,11 @@ def formula(person, period, parameters): # income under the federal PTC floor of 100% FPL, and within_band # already starts at the 138% FPL NJHPS floor. pays_premium = person("pays_aca_premium", period) - # NJHPS goes to enrollees otherwise eligible for APTC, so a person - # outside the federal tax family (one who can be claimed on another - # return, or a dependent of a return with such a filer) is not a - # member (26 U.S.C. 36B(c)(1)(D) is not an income test). + # Interpretation: NJHPS goes to enrollees otherwise eligible for APTC + # (Treasury 1332 addendum), so a person outside the federal tax family + # (one who can be claimed on another return, or a dependent of a + # return with such a filer) is not a member; 26 U.S.C. 36B(c)(1)(D) + # is not one of the income tests NJHPS replaces. tax_family_member = person("is_aca_tax_family_member", period) fstatus = person.tax_unit("filing_status", period) not_separate = fstatus != fstatus.possible_values.SEPARATE From 44bfb6df63e54762c603a0f7334b05a71e33291f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:41:08 -0400 Subject: [PATCH 6/7] Test the review r1 fixes and tighten the invariant test YAML: refund-only exception (family keeps the child), single claimable filer with the exception (no family), claimable but not claimed, lone claimable and unclaimed 17-year-old filers, household income without an out-of-family child's tax-exempt interest, the coverage-family premium cap (silver and bronze), CSR under the family-policy rule, Colorado's binding residual ($420), and the tax year's family size against a supplied prior-year size. Test inputs now use claimable_as_dependent_on_another_return. Property test: Hypothesis runs every year 2021-2026 and draws claimable-but- unclaimed spouses and the filing exception; the credit's monotonicity exemption is limited to crossing the 100% floor or dependents' income leaving household income; the benchmark check compares enrolled people, not counts; new identities for household income, the premium cap and CSR. Co-Authored-By: Claude Opus 5.5 --- .../test_claimable_filer_ptc_invariants.py | 150 +++++++---- .../gov/aca/csr/marketplace_csr_eligible.yaml | 111 ++++++++ .../baseline/gov/aca/eligibility/aca_fpg.yaml | 42 +++ .../gov/aca/eligibility/aca_magi.yaml | 212 +++++++++++++++ .../aca/eligibility/aca_tax_family_size.yaml | 92 +++++++ .../eligibility/is_aca_tax_family_member.yaml | 224 ++++++++++++++++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 246 ++++++++++++++++++ .../ptc/aca_ptc_coverage_family_premium.yaml | 195 ++++++++++++++ .../gov/aca/ptc/marketplace_net_premium.yaml | 77 ++++++ .../baseline/gov/aca/ptc/used_aca_ptc.yaml | 79 ++++++ .../co_premium_assistance.yaml | 83 ++++++ 11 files changed, 1463 insertions(+), 48 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py index c0ddc4c9fc2..afde18c3e02 100644 --- a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -1,28 +1,37 @@ """The premium tax credit for a filer who can be claimed as a dependent. -Someone another taxpayer can claim is not an applicable taxpayer (IRC -36B(c)(1)(D)) and is outside the tax family (26 CFR 1.36B-1(d)(2)); a return -with such a filer claims no dependents (IRC 152(b)(1)). The credit therefore -uses `aca_tax_family_size` for the poverty line and the coverage family's -benchmark `aca_ptc_slcsp`, and the state premium programs that wrap it follow. +No credit is allowed to someone another taxpayer can claim (IRC 36B(c)(1)(D)); +such a spouse is outside the tax family (26 CFR 1.36B-1(d)(2)), and a return +with such a filer claims no dependents (IRC 152(b)(1)) unless the would-be +claimant meets the refund-only filing exception. The credit therefore uses +`aca_tax_family_size` for the poverty line, household income without +out-of-family dependents (`aca_magi`), the coverage family's benchmark +(`aca_ptc_slcsp`) and its share of the premium +(`aca_ptc_coverage_family_premium`), and the state premium programs that wrap +it follow. -For couples drawn by Hypothesis and a seeded population, with either, both or -neither spouse claimed: +For couples drawn by Hypothesis in every year 2021-2026 (spouses claimable, +actually claimed or neither, with or without the filing exception) and a +seeded population in 2025 and 2026: 1. Swap invariance: each output is the same under either head/spouse labelling. 2. Identities: `aca_tax_family_size` equals the filers who cannot be claimed - plus the dependents when no filer can be claimed; nobody outside the tax - family is eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; a return where every - filer can be claimed has no credit; and a return where no filer can be - claimed keeps `slcsp`, the tax unit size and the tax unit's poverty line. + plus the dependents when one of them is an applicable taxpayer and no + filer is claimable outside the exception; nobody outside the tax family is + eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; `aca_magi` <= `medicaid_magi`; + used credit <= coverage-family premium <= selected premium; cost-sharing + reductions only when every enrollee is eligible for the credit; no family + member means no credit; and where no filer can be claimed, `aca_ptc_slcsp` + = `slcsp`, `aca_magi` = `medicaid_magi`, `aca_tax_family_size` = + `tax_unit_size` and `aca_fpg` = `tax_unit_fpg` for the prior year (these + simulations supply no prior-year size, so the two sizes agree). 3. Monotonicity: marking another filer as claimed never raises the tax family - size, never raises the benchmark while the enrollees stay the same, and - never raises the credit of a return that had an eligible member. Both can - rise otherwise, as intended: a couple below 100% of the two-person line can - be above it for a tax family of one, so IRC 36B allows the credit and the - unclaimable spouse leaves the coverage gap that pays_aca_premium assumes - nobody buys into. + size, never raises the benchmark while the same people are enrolled, and + never raises the credit, except in two intended cases: income crosses the + 100% floor (below it for two, above it for one, so IRC 36B allows a credit + that was not allowed), or out-of-family dependents' income leaves household + income. """ import numpy as np @@ -51,10 +60,13 @@ FEDERAL = [ "aca_tax_family_size", "aca_fpg", + "aca_magi", "aca_magi_fraction", "slcsp", "aca_ptc_slcsp", "aca_ptc", + "aca_ptc_coverage_family_premium", + "used_aca_ptc", "marketplace_csr_eligible", ] STATE_WRAPS = [ @@ -73,11 +85,16 @@ @st.composite def couples(draw): + claimed = draw(st.sampled_from(CLAIM_PATTERNS)) + # Claimable whenever claimed, and sometimes claimable but not claimed. + extra = draw(st.sampled_from(CLAIM_PATTERNS)) return { "state": draw(st.sampled_from(STATES)), "adults": [draw(adults()), draw(adults())], "dependents": draw(st.lists(dependents(), max_size=2)), - "claimed": draw(st.sampled_from(CLAIM_PATTERNS)), + "claimed": claimed, + "claimable": (claimed[0] or extra[0], claimed[1] or extra[1]), + "exception": draw(st.sampled_from(CLAIM_PATTERNS)), } @@ -109,13 +126,37 @@ def _tax_unit_sum(sim, name, year): return np.asarray(sim.calculate(name, year, map_to="tax_unit"), dtype=float) +def _tax_unit_sum_array(sim, values): + tax_units = sim.populations["tax_unit"] + return np.bincount( + np.asarray(tax_units.members_entity_id), + weights=np.asarray(values, dtype=float), + minlength=tax_units.count, + ) + + +def _enrollment_signature(sim, year): + # Which members are enrolled, as one number per tax unit, so variants + # can be compared person by person rather than by count. + position = np.asarray(sim.populations["tax_unit"].members_position) + enrolled = np.asarray(sim.calculate("pays_aca_premium", year), dtype=float) + return _tax_unit_sum_array(sim, enrolled * 2.0**position) + + def _identities(sim, year): - claimed_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + claimable_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 + blocked = ( + _calc( + sim, "head_or_spouse_is_dependent_elsewhere_without_filing_exception", year + ) + > 0 + ) independent = _calc(sim, "head_spouse_count_not_dependent_elsewhere", year) dependents_count = _calc(sim, "tax_unit_count_dependents", year) size = _calc(sim, "aca_tax_family_size", year) + claims_dependents = (independent > 0) & ~blocked np.testing.assert_array_equal( - size, independent + np.where(claimed_filer, 0, dependents_count) + size, independent + np.where(claims_dependents, dependents_count, 0) ) eligible = np.asarray(sim.calculate("is_aca_ptc_eligible", year)) member = np.asarray(sim.calculate("is_aca_tax_family_member", year)) @@ -124,10 +165,24 @@ def _identities(sim, year): benchmark = _calc(sim, "aca_ptc_slcsp", year) assert (benchmark >= -TOLERANCE).all() assert (benchmark <= slcsp + TOLERANCE).all(), "benchmark above slcsp" - nobody_in_family = size == 0 - assert not _calc(sim, "aca_ptc", year)[nobody_in_family].any() - unclaimed = ~claimed_filer + aca_magi = _calc(sim, "aca_magi", year) + medicaid_magi = _calc(sim, "medicaid_magi", year) + assert (aca_magi <= medicaid_magi + TOLERANCE).all() + premium = _calc(sim, "selected_marketplace_plan_premium_proxy", year) + family_premium = _calc(sim, "aca_ptc_coverage_family_premium", year) + used = _calc(sim, "used_aca_ptc", year) + assert (used <= family_premium + TOLERANCE).all() + assert (family_premium <= premium + TOLERANCE).all() + csr = _calc(sim, "marketplace_csr_eligible", year) > 0 + enrolled = np.asarray(sim.calculate("pays_aca_premium", year)) + ineligible_enrollees = _tax_unit_sum_array(sim, enrolled & ~eligible) + assert not (csr & (ineligible_enrollees > 0)).any() + assert not _calc(sim, "aca_ptc", year)[size == 0].any() + unclaimed = ~claimable_filer np.testing.assert_allclose(benchmark[unclaimed], slcsp[unclaimed], atol=TOLERANCE) + np.testing.assert_allclose( + aca_magi[unclaimed], medicaid_magi[unclaimed], atol=TOLERANCE + ) np.testing.assert_array_equal( size[unclaimed], _calc(sim, "tax_unit_size", year)[unclaimed] ) @@ -143,13 +198,13 @@ def check_swap(units, year, outputs, require_coverage=False): # The batch must reach the rule: returns with exactly one claimable # filer that still get a credit and, from 2026 when the modeled state # programs start, a state wrap paid on one. - one_claimed = ( + one_claimable = ( _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 ) & (_calc(sim, "head_spouse_count_not_dependent_elsewhere", year) == 1) - assert (_calc(sim, "aca_ptc", year)[one_claimed] > 0).any() + assert (_calc(sim, "aca_ptc", year)[one_claimable] > 0).any() if year >= 2026: wraps = sum(_calc(sim, name, year) for name in STATE_WRAPS) - assert (wraps[one_claimed] > 0).any() + assert (wraps[one_claimable] > 0).any() values = {name: _calc(sim, name, year) for name in outputs} values["eligible members"] = _tax_unit_sum(sim, "is_aca_ptc_eligible", year) for name, value in values.items(): @@ -170,45 +225,44 @@ def check_swap(units, year, outputs, require_coverage=False): def check_monotone(units, year): sim = Simulation(situation=_situation(units, year, _monotone_variants)) - had_eligible = (_tax_unit_sum(sim, "is_aca_ptc_eligible", year) > 0).reshape(-1, 4) - enrollees = _tax_unit_sum(sim, "pays_aca_premium", year).reshape(-1, 4) - for name, guard in ( - ("aca_tax_family_size", None), - ("aca_ptc_slcsp", "same enrollees"), - ("aca_ptc", "had eligible"), - ): + fraction = _calc(sim, "aca_magi_fraction", year).reshape(-1, 4) + magi = _calc(sim, "aca_magi", year).reshape(-1, 4) + enrollees = _enrollment_signature(sim, year).reshape(-1, 4) + for name in ("aca_tax_family_size", "aca_ptc_slcsp", "aca_ptc"): values = _calc(sim, name, year).reshape(-1, 4) - nobody, first, second, both = values.T - for more, fewer, base, label in ( - (first, nobody, 0, "first adult"), - (second, nobody, 0, "second adult"), - (both, first, 1, "both after first"), - (both, second, 2, "both after second"), + for more, fewer, label in ( + (1, 0, "first adult"), + (2, 0, "second adult"), + (3, 1, "both after first"), + (3, 2, "both after second"), ): - rises = more > fewer + TOLERANCE - if guard == "had eligible": - rises &= had_eligible[:, base] - elif guard == "same enrollees": - more_index = {"first adult": 1, "second adult": 2}.get(label, 3) - rises &= enrollees[:, base] == enrollees[:, more_index] + rises = values[:, more] > values[:, fewer] + TOLERANCE + if name == "aca_ptc_slcsp": + rises &= enrollees[:, more] == enrollees[:, fewer] + if name == "aca_ptc": + crosses_floor = (fraction[:, fewer] < 1) & (fraction[:, more] >= 1) + income_left = magi[:, more] < magi[:, fewer] - TOLERANCE + rises &= ~crosses_floor & ~income_left assert not rises.any(), ( f"{name} in {year} rises when the {label} is claimed " "elsewhere: " + ", ".join( - f"{units[i]['state']} {fewer[i]:.2f} -> {more[i]:.2f}" + f"{units[i]['state']} {values[i, fewer]:.2f} -> " + f"{values[i, more]:.2f}" for i in np.flatnonzero(rises)[:10] ) ) _identities(sim, year) +@pytest.mark.parametrize("year", YEARS) @settings( - max_examples=4, + max_examples=2, deadline=None, derandomize=True, suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], ) -@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=15)) +@given(st.lists(couples(), min_size=6, max_size=15)) def test_claimable_filer_ptc_is_label_free(year, units): check_swap(units, year, OUTPUTS) diff --git a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml index b3780f6fe83..de6bf23e9d8 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml @@ -28,3 +28,114 @@ selected_marketplace_plan_actuarial_value: 0.7 marketplace_csr_actuarial_value: 0 marketplace_effective_actuarial_value: 0.7 + +- name: 2025 one policy covering a claimable spouse gets no cost-sharing reductions (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 45 CFR 155.305(g)(3): a policy gets only the category all its + # enrollees qualify for; the model prices one policy per tax unit. + person_receives_aca: [false, true] + marketplace_csr_eligible: false + +- name: 2025 one policy covering a claimable spouse gets no cost-sharing reductions (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: TX + output: + person_receives_aca: [true, false] + marketplace_csr_eligible: false + +- name: 2025 one policy whose enrollees are all in the family keeps cost-sharing reductions + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: TX + output: + marketplace_csr_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml index 8f2df037394..ea84ca29d74 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_fpg.yaml @@ -236,3 +236,45 @@ state_code: TX output: aca_fpg: 15_650 + +- name: 2025 the poverty line uses the tax year's family size, not a supplied prior-year size + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + tax_unit_size: {2024: 2, 2025: 3} + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # Form 8962 line 1 is the tax year's family; line 4 takes the 2024 + # guideline for it: 15,060 + 2 * 5,380. tax_unit_fpg for 2024 would + # have used the supplied 2024 size of 2 (20,440). + aca_tax_family_size: 3 + aca_fpg: 25_820 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml index 1a5d780bf1d..eb7114d16ee 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml @@ -41,3 +41,215 @@ # Form 8962 Worksheet 1-1 line 3 takes Form 2555 lines 45 and 50. medicaid_magi: 65_000 aca_magi: 65_000 + +- name: 2025 joint return with a claimable spouse leaves the child's tax-exempt interest out of household income (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 26 CFR 1.36B-1(e)(1): household income counts other individuals only + # if they are in the family; this return's child is not (IRC 152(b)(1)). + adjusted_gross_income: 20_000 + medicaid_magi: 40_000 + aca_magi: 20_000 + +- name: 2025 joint return with a claimable spouse leaves the child's tax-exempt interest out of household income (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + adjusted_gross_income: 20_000 + medicaid_magi: 40_000 + aca_magi: 20_000 + +- name: 2025 joint return with neither spouse claimable keeps household income as medicaid_magi + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + aca_magi: 40_000 + +- name: 2025 refund-only exception keeps the child in the family and their income in household income (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + aca_magi: 40_000 + +- name: 2025 refund-only exception keeps the child in the family and their income in household income (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_magi: 40_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml index 3133369a77d..07a0ecbaf5e 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_tax_family_size.yaml @@ -231,3 +231,95 @@ state_code: TX output: aca_tax_family_size: 2 + +- name: "2025 joint return, claimable spouse with the refund-only exception, two children: family of three (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child, child2] + state_code: TX + output: + # Publication 501 (2025) p.11, Dependent Taxpayer Test: the return can claim + # dependents when the would-be claimant files only for a refund. + aca_tax_family_size: 3 + +- name: "2025 joint return, claimable spouse with the refund-only exception, two children: family of three (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + child2: + age: 7 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child, child2] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child, child2] + state_code: TX + output: + aca_tax_family_size: 3 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml index 7a150cdf659..14c88a14ceb 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -207,3 +207,227 @@ state_code: TX output: is_aca_tax_family_member: [true, true] + +- name: "2025 joint return, claimable spouse whose claimant meets the refund-only exception: spouse out, child in (the claimable spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # Publication 501 (2025) p.11, Dependent Taxpayer Test: the return can claim + # dependents when the would-be claimant files only for a refund. + # Form 1040 line 12a still marks the spouse as claimable (2025 instructions p.33). + is_aca_tax_family_member: [false, true, true] + +- name: "2025 joint return, claimable spouse whose claimant meets the refund-only exception: spouse out, child in (the claimable spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + is_aca_tax_family_member: [true, false, true] + +- name: 2025 single claimable filer with the refund-only exception has no applicable taxpayer, so no family + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person_a, child] + filing_status: SINGLE + households: + household: + members: [person_a, child] + state_code: TX + output: + # IRC 36B(c)(1)(D): no credit to someone another taxpayer can claim. + is_aca_tax_family_member: [false, false] + +- name: 2025 joint return, spouse claimable but not actually claimed is still out of the family (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + claimed_as_dependent_on_another_return: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 26 CFR 1.36B-2(b)(3): 'may claim', whether or not claimed. + is_aca_tax_family_member: [false, true] + +- name: 2025 joint return, spouse claimable but not actually claimed is still out of the family (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + claimed_as_dependent_on_another_return: false + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: TX + output: + is_aca_tax_family_member: [true, false] + +- name: "2025 tax unit with neither head nor spouse: a claimable 17-year-old filer is out of the family" + period: 2025 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + households: + household: + members: [minor] + state_code: TX + output: + # Role inference gives a lone minor no head; own claimability decides. + is_tax_unit_head: [false] + is_aca_tax_family_member: [false] + +- name: "2025 tax unit with neither head nor spouse: an unclaimed 17-year-old filer stays in the family" + period: 2025 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + households: + household: + members: [minor] + state_code: TX + output: + is_tax_unit_head: [false] + is_aca_tax_family_member: [true] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 462efa51568..8d7f816bc8d 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -726,3 +726,249 @@ state_code: TX output: aca_ptc: 0 + +- name: 2025 child's tax-exempt interest no longer cuts the credit of a return with a claimable spouse (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 20,000 / 15,060 = 132% FPL: 0% contribution; benchmark 6,000 (child out). + aca_magi: 20_000 + aca_magi_fraction: 1.32 + aca_ptc: 6000 + +- name: 2025 child's tax-exempt interest no longer cuts the credit of a return with a claimable spouse (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + employment_income: 10_000 + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_exempt_interest_income: 20_000 + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_magi: 20_000 + aca_magi_fraction: 1.32 + aca_ptc: 6000 + +- name: "2025 refund-only exception: credit for the other spouse and the child (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 30_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # 30,000 / 20,440 = 146% FPL: 0% contribution; benchmark 6,000 + 3,000. + aca_tax_family_size: 2 + aca_ptc_slcsp: 9000 + aca_ptc: 9000 + +- name: "2025 refund-only exception: credit for the other spouse and the child (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 3000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 30_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + aca_tax_family_size: 2 + aca_ptc_slcsp: 9000 + aca_ptc: 9000 + +- name: 2025 claimable 17-year-old filing alone gets no credit + period: 2025 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [minor] + state_code: TX + output: + is_aca_ptc_eligible: [false] + aca_ptc: 0 + +- name: 2025 unclaimed 17-year-old filing alone keeps the credit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + minor: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [minor] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [minor] + state_code: TX + output: + is_aca_ptc_eligible: [true] + aca_ptc: 6000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml new file mode 100644 index 00000000000..bf7ceae45b0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml @@ -0,0 +1,195 @@ +- name: "2025 joint return with a claimable spouse on the policy: half the premium is the coverage family's (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # Silver plan at 0.75 of the 12,000 benchmark: 9,000. 26 CFR 1.36B-3(h)(1): + # the coverage family's share is 6,000 / 12,000, so 4,500. + selected_marketplace_plan_premium_proxy: 9000 + aca_ptc_coverage_family_premium: 4500 + +- name: "2025 joint return with a claimable spouse on the policy: half the premium is the coverage family's (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + selected_marketplace_plan_premium_proxy: 9000 + aca_ptc_coverage_family_premium: 4500 + +- name: "2025 joint return with neither spouse claimable: the whole premium is the coverage family's" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_coverage_family_premium: 9000 + +- name: 2025 bronze plan is allocated by the same benchmark share (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_category: BRONZE + lcbp: 7000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 7,000 * 6,000 / 12,000. + aca_ptc_coverage_family_premium: 3500 + +- name: 2025 bronze plan is allocated by the same benchmark share (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_category: BRONZE + lcbp: 7000 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc_coverage_family_premium: 3500 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml index ddf141f5162..c5a2aa7a740 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml @@ -55,3 +55,80 @@ selected_marketplace_plan_premium_proxy: 5_000 used_aca_ptc: 3_000 marketplace_net_premium: 2_000 + +- name: 2025 the claimable spouse's share of the premium stays unsubsidized (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # 9,000 - 4,500. + marketplace_net_premium: 4500 + +- name: 2025 the claimable spouse's share of the premium stays unsubsidized (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + marketplace_net_premium: 4500 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml index c1f36cf129c..776d01b85dd 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml @@ -74,3 +74,82 @@ selected_marketplace_plan_premium_proxy: 7_000 used_aca_ptc: 7_000 unused_aca_ptc: 2_200 + +- name: 2025 credit is capped at the coverage family's share of the premium (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + # min(6,000, 4,500); before, the cap was the whole 9,000 premium. + aca_ptc: 6000 + used_aca_ptc: 4500 + +- name: 2025 credit is capped at the coverage family's share of the premium (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + claimable_as_dependent_on_another_return: true + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.75 + households: + household: + members: [person_b, person_a] + state_code: TX + output: + aca_ptc: 6000 + used_aca_ptc: 4500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml index 0df47cd45e4..0b311e29096 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/doi/premium_assistance/co_premium_assistance.yaml @@ -843,3 +843,86 @@ output: # Neither spouse is in a federal tax family. co_premium_assistance: 0 + +- name: 2026 CO joint return with one claimable spouse where the residual premium binds (the claimable spouse is the head) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 20_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b] + state_code: CO + output: + # 20,000 / 15,650 = 127% FPL, 2.1%: contribution 420, aca_ptc 5,580. + # min(12 * 80, 6,000 - 5,580) = 420; the whole-enrollment benchmark + # would give min(960, 12,000 - 5,580) = 960. + aca_ptc: 5580 + co_premium_assistance: 420 + +- name: 2026 CO joint return with one claimable spouse where the residual premium binds (the claimable spouse is the spouse) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + aca_magi: 20_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a] + state_code: CO + output: + aca_ptc: 5580 + co_premium_assistance: 420 From a4d97502eb88caebdbfe7a33f4039885b05ebf80 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 08:12:09 -0400 Subject: [PATCH 7/7] Address review r2: role-less filers, coverage-family pricing and allocation, MAGI floor - A tax unit with neither a head nor a spouse is filed by its oldest member. When that member can be claimed elsewhere, nobody is in the tax family. Before, a claimable 17-year-old filing with an infant got $12,000. - The coverage family is priced on its own. Its children are ranked for the three-oldest-children rule (45 CFR 147.102(c)(1)), and its family tier is built from its own members. - A shared premium is allocated by each family's benchmark (26 CFR 1.36B-3(h)(1)). The denominator is the coverage family's benchmark plus the benchmark of the enrollees outside it, not the whole policy's tier. An empty coverage family gets no share of the premium. - Out-of-family dependents' tax-exempt interest and Social Security are removed before the single zero floor. A negative benefit no longer cuts household income. - is_aca_marketplace_enrollee (enrolled) is split from pays_aca_premium (charged an age-rated premium). The family-tier child tests no longer depend on payment. - Tests: YAML for each case under both head/spouse labellings. The property test now gives dependents tax-exempt interest and Social Security. It checks aca_magi against an expectation built from the inputs, and it exempts only that removal from monotonicity. Monotonicity runs under Hypothesis in every year 2021-2026. The "unchanged" identities apply only to returns with a head or spouse. Co-Authored-By: Claude Opus 5.5 --- .../test_claimable_filer_ptc_invariants.py | 190 +++++++++++++++--- .../gov/aca/eligibility/aca_magi.yaml | 85 ++++++++ .../is_aca_marketplace_enrollee.yaml | 64 ++++++ .../eligibility/is_aca_tax_family_member.yaml | 66 ++++++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 71 +++++++ .../ptc/aca_ptc_coverage_family_premium.yaml | 144 +++++++++++++ .../baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml | 132 ++++++++++++ .../variables/gov/aca/eligibility/aca_magi.py | 29 ++- .../is_aca_coverage_family_member.py | 14 +- .../is_aca_marketplace_enrollee.py | 56 ++++++ .../eligibility/is_aca_tax_family_member.py | 17 +- .../gov/aca/eligibility/pays_aca_premium.py | 47 +---- .../ptc/aca_ptc_coverage_family_premium.py | 37 ++-- .../variables/gov/aca/slspc/aca_ptc_slcsp.py | 69 +++++-- .../is_aca_family_tier_dependent_child.py | 29 ++- .../slspc/is_aca_ny_age_29_dependent_child.py | 25 ++- .../aca/slspc/slcsp_family_tier_category.py | 5 +- .../aca/slspc/slcsp_family_tier_multiplier.py | 10 +- 18 files changed, 953 insertions(+), 137 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml create mode 100644 policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py diff --git a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py index afde18c3e02..134745ca608 100644 --- a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -11,27 +11,36 @@ it follow. For couples drawn by Hypothesis in every year 2021-2026 (spouses claimable, -actually claimed or neither, with or without the filing exception) and a -seeded population in 2025 and 2026: +actually claimed or neither, with or without the filing exception, and +dependents with tax-exempt interest and Social Security) and a seeded +population in 2025 and 2026 that includes review r2's family and premium +cases: 1. Swap invariance: each output is the same under either head/spouse labelling. 2. Identities: `aca_tax_family_size` equals the filers who cannot be claimed plus the dependents when one of them is an applicable taxpayer and no filer is claimable outside the exception; nobody outside the tax family is - eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; `aca_magi` <= `medicaid_magi`; - used credit <= coverage-family premium <= selected premium; cost-sharing - reductions only when every enrollee is eligible for the credit; no family - member means no credit; and where no filer can be claimed, `aca_ptc_slcsp` - = `slcsp`, `aca_magi` = `medicaid_magi`, `aca_tax_family_size` = - `tax_unit_size` and `aca_fpg` = `tax_unit_fpg` for the prior year (these - simulations supply no prior-year size, so the two sizes agree). -3. Monotonicity: marking another filer as claimed never raises the tax family - size, never raises the benchmark while the same people are enrolled, and - never raises the credit, except in two intended cases: income crosses the - 100% floor (below it for two, above it for one, so IRC 36B allows a credit - that was not allowed), or out-of-family dependents' income leaves household - income. + eligible; 0 <= `aca_ptc_slcsp` <= `slcsp`; `aca_magi` is AGI plus the + MAGI additions, less the tax-exempt interest and Social Security of + dependents outside the family, floored at zero once (computed here from + the inputs, not from the model's family variables); used credit <= + coverage-family premium <= selected premium; no family member means no + credit and no coverage-family premium; cost-sharing reductions only when + every enrollee is eligible for the credit; and on a return with a head or + spouse where neither can be claimed, `aca_ptc_slcsp` = `slcsp`, + `aca_magi` = `medicaid_magi`, `aca_tax_family_size` = `tax_unit_size` and + `aca_fpg` = `tax_unit_fpg` for the prior year (these simulations supply no + prior-year size, so the two sizes agree). A unit with neither a head nor a + spouse is filed by its oldest member, so a claimable minor changes it. +3. Monotonicity, for the drawn couples in every year and the seeded + population: marking another filer as claimable never raises the tax + family size, never raises the benchmark while the same people are + enrolled, and never raises the credit, except in two intended cases: + income crosses the 100% floor (below it for two, above it for one, so IRC + 36B allows a credit that was not allowed), or dependents leave the family + and their tax-exempt interest and Social Security leave household income + (identity 2 checks the amount removed). """ import numpy as np @@ -51,6 +60,7 @@ _swap_variants, adults, dependents, + money, ) # Texas (age curve, no expansion), New York and Vermont (family tiers), and @@ -81,6 +91,18 @@ "wa_cascade_care_savings", ] OUTPUTS = FEDERAL + STATE_WRAPS +INCOME_SEED = 20261012 + + +@st.composite +def ptc_dependents(draw): + # Tax-exempt interest and Social Security count in household income only + # while the dependent is in the family. + return { + **draw(dependents()), + "tax_exempt_interest_income": draw(money(20_000)), + "social_security_survivors": draw(money(12_000)), + } @st.composite @@ -91,7 +113,7 @@ def couples(draw): return { "state": draw(st.sampled_from(STATES)), "adults": [draw(adults()), draw(adults())], - "dependents": draw(st.lists(dependents(), max_size=2)), + "dependents": draw(st.lists(ptc_dependents(), max_size=2)), "claimed": claimed, "claimable": (claimed[0] or extra[0], claimed[1] or extra[1]), "exception": draw(st.sampled_from(CLAIM_PATTERNS)), @@ -122,6 +144,67 @@ def _ptc_cases(states): return cases +def _review_r2_cases(): + # A New York family-tier policy and a Texas coverage family of one adult + # and three children, each beside a claimable spouse whose claimant meets + # the refund-only exception, and dependents whose tax-exempt interest or + # negative Social Security leaves household income. + adult = {"age": 30, "employment_income": 11_000.0} + exception = {"claimable": (True, False), "exception": (True, False)} + return [ + { + "state": "NY", + "adults": [adult, adult], + "dependents": [{"age": 10}], + "claimed": (False, False), + **exception, + }, + { + "state": "TX", + "adults": [ + {"age": 19, "employment_income": 11_000.0}, + {"age": 30, "employment_income": 29_000.0}, + ], + "dependents": [{"age": 10}, {"age": 8}, {"age": 6}], + "claimed": (False, False), + **exception, + }, + { + "state": "TX", + "adults": [adult, adult], + "dependents": [{"age": 10, "tax_exempt_interest_income": 20_000.0}], + "claimed": (True, False), + }, + { + "state": "TX", + "adults": [adult, adult], + "dependents": [{"age": 10, "social_security_survivors": -30_000.0}], + "claimed": (True, False), + }, + ] + + +def _with_dependent_income(units): + # Some seeded dependents get tax-exempt interest or Social Security. + rng = np.random.default_rng(INCOME_SEED) + out = [] + for unit in units: + dependents_with_income = [] + for amounts in unit["dependents"]: + amounts = dict(amounts) + if rng.random() < 0.5: + amounts["tax_exempt_interest_income"] = float( + round(rng.uniform(1, 20_000)) + ) + if rng.random() < 0.3: + amounts["social_security_survivors"] = float( + round(rng.uniform(1, 12_000)) + ) + dependents_with_income.append(amounts) + out.append({**unit, "dependents": dependents_with_income}) + return out + + def _tax_unit_sum(sim, name, year): return np.asarray(sim.calculate(name, year, map_to="tax_unit"), dtype=float) @@ -143,6 +226,40 @@ def _enrollment_signature(sim, year): return _tax_unit_sum_array(sim, enrolled * 2.0**position) +def _expected_aca_magi(sim, year): + # 26 CFR 1.36B-1(e)(1): household income counts a dependent's income only + # if the return claims them. A return claims none when no filer is an + # applicable taxpayer or when a filer can be claimed and the claimant + # does not meet the refund-only exception. Built from the inputs. + filer = np.asarray(sim.calculate("is_tax_unit_head_or_spouse", year)) + claimable = np.asarray( + sim.calculate("claimable_as_dependent_on_another_return", year) + ) + exception = np.asarray(sim.calculate("dependent_claimant_filing_exception", year)) + independent = _tax_unit_sum_array(sim, filer & ~claimable) + blocked = _tax_unit_sum_array(sim, filer & claimable & ~exception) > 0 + dependent_income = _tax_unit_sum_array( + sim, + ~filer + * ( + _calc(sim, "tax_exempt_interest_income", year) + + _calc(sim, "social_security", year) + ), + ) + removed = np.where((independent == 0) | blocked, dependent_income, 0) + unfloored = _calc(sim, "adjusted_gross_income", year) + additions = sim.tax_benefit_system.parameters( + f"{year}-01-01" + ).gov.hhs.medicaid.income.modification + for name in additions: + if sim.tax_benefit_system.variables[name].entity.key == "person": + unfloored = unfloored + _tax_unit_sum(sim, name, year) + else: + unfloored = unfloored + _calc(sim, name, year) + roleful = _tax_unit_sum_array(sim, filer) > 0 + return np.maximum(0, unfloored - removed), removed, roleful + + def _identities(sim, year): claimable_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0 blocked = ( @@ -167,7 +284,10 @@ def _identities(sim, year): assert (benchmark <= slcsp + TOLERANCE).all(), "benchmark above slcsp" aca_magi = _calc(sim, "aca_magi", year) medicaid_magi = _calc(sim, "medicaid_magi", year) - assert (aca_magi <= medicaid_magi + TOLERANCE).all() + expected_magi, _, roleful = _expected_aca_magi(sim, year) + np.testing.assert_allclose( + aca_magi[roleful], expected_magi[roleful], atol=TOLERANCE + ) premium = _calc(sim, "selected_marketplace_plan_premium_proxy", year) family_premium = _calc(sim, "aca_ptc_coverage_family_premium", year) used = _calc(sim, "used_aca_ptc", year) @@ -178,7 +298,8 @@ def _identities(sim, year): ineligible_enrollees = _tax_unit_sum_array(sim, enrolled & ~eligible) assert not (csr & (ineligible_enrollees > 0)).any() assert not _calc(sim, "aca_ptc", year)[size == 0].any() - unclaimed = ~claimable_filer + assert not family_premium[size == 0].any() + unclaimed = roleful & ~claimable_filer np.testing.assert_allclose(benchmark[unclaimed], slcsp[unclaimed], atol=TOLERANCE) np.testing.assert_allclose( aca_magi[unclaimed], medicaid_magi[unclaimed], atol=TOLERANCE @@ -223,10 +344,18 @@ def check_swap(units, year, outputs, require_coverage=False): _identities(sim, year) -def check_monotone(units, year): - sim = Simulation(situation=_situation(units, year, _monotone_variants)) +def check_monotone(units, year, require_removal=False): + # Each filer is claimable exactly when claimed in the variant; drawn + # filing exceptions stay. + sim = Simulation( + situation=_situation( + units, year, _monotone_variants, explicit_claimability=True + ) + ) fraction = _calc(sim, "aca_magi_fraction", year).reshape(-1, 4) - magi = _calc(sim, "aca_magi", year).reshape(-1, 4) + removed = _expected_aca_magi(sim, year)[1].reshape(-1, 4) + if require_removal: + assert (removed > 0).any(), "no dependent income leaves household income" enrollees = _enrollment_signature(sim, year).reshape(-1, 4) for name in ("aca_tax_family_size", "aca_ptc_slcsp", "aca_ptc"): values = _calc(sim, name, year).reshape(-1, 4) @@ -241,7 +370,7 @@ def check_monotone(units, year): rises &= enrollees[:, more] == enrollees[:, fewer] if name == "aca_ptc": crosses_floor = (fraction[:, fewer] < 1) & (fraction[:, more] >= 1) - income_left = magi[:, more] < magi[:, fewer] - TOLERANCE + income_left = removed[:, more] > removed[:, fewer] + TOLERANCE rises &= ~crosses_floor & ~income_left assert not rises.any(), ( f"{name} in {year} rises when the {label} is claimed " @@ -267,12 +396,25 @@ def test_claimable_filer_ptc_is_label_free(year, units): check_swap(units, year, OUTPUTS) +@pytest.mark.parametrize("year", YEARS) +@settings( + max_examples=2, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(couples(), min_size=6, max_size=15)) +def test_claimable_filer_ptc_is_monotone(year, units): + check_monotone(units, year) + + @pytest.mark.parametrize("year", [2025, 2026]) def test_claimable_filer_ptc_seeded_population(year): units = ( - _seeded_couples(STATES, per_state=2) + _with_dependent_income(_seeded_couples(STATES, per_state=2)) + _crafted_cases(STATES, review_case_only=True) + _ptc_cases(STATES) + + _review_r2_cases() ) check_swap(units, year, OUTPUTS, require_coverage=True) - check_monotone(units, year) + check_monotone(units, year, require_removal=True) diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml index eb7114d16ee..d2bb20d5090 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_magi.yaml @@ -253,3 +253,88 @@ state_code: TX output: aca_magi: 40_000 + +- name: 2025 an excluded dependent's negative Social Security is removed before the zero floor (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + social_security: -30_000 + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: TX + output: + # max(0, 20,000 - 30,000 - (-30,000)) = 20,000. + medicaid_magi: 0 + aca_magi: 20_000 + +- name: 2025 an excluded dependent's negative Social Security is removed before the zero floor (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + employment_income: 10_000 + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + social_security: -30_000 + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: TX + output: + medicaid_magi: 0 + aca_magi: 20_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml new file mode 100644 index 00000000000..b8dee264d14 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_marketplace_enrollee.yaml @@ -0,0 +1,64 @@ +- name: 2025 a fourth child is enrolled but pays no age-rated premium + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child2: + age: 8 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child3: + age: 6 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child4: + age: 4 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2, child3, child4] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child, child2, child3, child4] + state_code: TX + output: + # 45 CFR 147.102(c)(1): premiums for the three oldest children only. + is_aca_marketplace_enrollee: [true, true, true, true, true, true] + pays_aca_premium: [true, true, true, true, true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml index 14c88a14ceb..4e7fbfc067e 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/is_aca_tax_family_member.yaml @@ -431,3 +431,69 @@ output: is_tax_unit_head: [false] is_aca_tax_family_member: [true] + +- name: "2025 claimable 17-year-old filing with an infant: neither is in a family" + period: 2025 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + households: + household: + members: [parent, infant] + state_code: TX + output: + # Role inference makes the oldest adult the head, so this unit has none; the + # oldest member is taken as the filer (IRC 36B(c)(1)(D)). + is_tax_unit_head: [false, false] + is_aca_tax_family_member: [false, false] + +- name: "2025 unclaimed 17-year-old filing with an infant: both are in the family" + period: 2025 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + households: + household: + members: [parent, infant] + state_code: TX + output: + is_aca_tax_family_member: [true, true] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 8d7f816bc8d..01b99f555c2 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -972,3 +972,74 @@ output: is_aca_ptc_eligible: [true] aca_ptc: 6000 + +- name: 2025 claimable 17-year-old filing with an infant gets no credit + period: 2025 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + claimable_as_dependent_on_another_return: true + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [parent, infant] + state_code: TX + output: + # Role inference makes the oldest adult the head, so this unit has none; the + # oldest member is taken as the filer (IRC 36B(c)(1)(D)). + aca_ptc: 0 + +- name: 2025 unclaimed 17-year-old filing with an infant gets the credit for both + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 17 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + infant: + age: 1 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + pays_aca_premium: true + slcsp_age_curve_amount_person: 6000 + tax_units: + tax_unit: + members: [parent, infant] + filing_status: SINGLE + aca_magi: 22_590 + tax_unit_is_filer: true + households: + household: + members: [parent, infant] + state_code: TX + output: + # 22,590 / 20,440 = 110% FPL: 0% contribution; 2 * 6,000. + aca_ptc: 12_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml index bf7ceae45b0..7d80d1157d5 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc_coverage_family_premium.yaml @@ -193,3 +193,147 @@ state_code: TX output: aca_ptc_coverage_family_premium: 3500 + +- name: "2025 NY policy with a claimable spouse (refund-only exception) and a child: allocate by each family's tier (the claimable spouse is the head)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.5 + households: + household: + members: [person_a, person_b, child] + state_code: NY + slcsp_age_0: 6000 + slcsp_rating_area: 3 + output: + # NY tiers on 6,000: whole policy 2.85 = 17,100; coverage family (one + # adult and a child) 1.70 = 10,200; the claimable spouse alone 1.00 = + # 6,000. 26 CFR 1.36B-3(h)(1): 8,550 * 10,200 / (10,200 + 6,000). + slcsp: 17_100 + aca_ptc_slcsp: 10_200 + selected_marketplace_plan_premium_proxy: 8550 + aca_ptc_coverage_family_premium: 5383.33 + +- name: "2025 NY policy with a claimable spouse (refund-only exception) and a child: allocate by each family's tier (the claimable spouse is the spouse)" + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + aca_magi: 22_000 + tax_unit_is_filer: true + selected_marketplace_plan_benchmark_ratio: 0.5 + households: + household: + members: [person_b, person_a, child] + state_code: NY + slcsp_age_0: 6000 + slcsp_rating_area: 3 + output: + slcsp: 17_100 + aca_ptc_slcsp: 10_200 + selected_marketplace_plan_premium_proxy: 8550 + aca_ptc_coverage_family_premium: 5383.33 + +- name: "2025 both spouses claimable: no coverage family, so none of a bronze premium" + period: 2025 + input: + people: + person_a: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + aca_magi: 22_590 + tax_unit_is_filer: true + selected_marketplace_plan_category: BRONZE + lcbp: 7000 + households: + household: + members: [person_a, person_b] + state_code: TX + output: + aca_ptc_coverage_family_premium: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml index 51ae163bac9..5a70e1a382f 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/slcsp/aca_ptc_slcsp.yaml @@ -370,3 +370,135 @@ output: slcsp: 2135.6 aca_ptc_slcsp: 760 + +- name: 2025 coverage family's three oldest children are charged once a claimable 19-year-old spouse is out (the claimable spouse is the head) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_a: + age: 19 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child2: + age: 8 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child3: + age: 6 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_a, person_b, child, child2, child3] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + households: + household: + members: [person_a, person_b, child, child2, child3] + state_code: TX + slcsp_age_0: 1000 + slcsp_rating_area: 1 + output: + # 45 CFR 147.102(c)(1): the three oldest covered children. The whole + # tax unit ranks the 19-year-old first, so the youngest child pays no + # premium in slcsp; the coverage family (the other spouse and all + # three children) charges all three. Default age curve on 1,000. + pays_aca_premium: [true, true, true, true, false] + is_aca_marketplace_enrollee: [true, true, true, true, true] + slcsp: 4713.8 + aca_ptc_slcsp: 4483.7 + +- name: 2025 coverage family's three oldest children are charged once a claimable 19-year-old spouse is out (the claimable spouse is the spouse) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person_b: + age: 30 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 19 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + claimable_as_dependent_on_another_return: true + dependent_claimant_filing_exception: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child2: + age: 8 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + child3: + age: 6 + is_medicaid_eligible: false + is_chip_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_basic_health_program_eligible: false + tax_units: + tax_unit: + members: [person_b, person_a, child, child2, child3] + filing_status: JOINT + aca_magi: 40_000 + tax_unit_is_filer: true + households: + household: + members: [person_b, person_a, child, child2, child3] + state_code: TX + slcsp_age_0: 1000 + slcsp_rating_area: 1 + output: + pays_aca_premium: [true, true, true, true, false] + is_aca_marketplace_enrollee: [true, true, true, true, true] + slcsp: 4713.8 + aca_ptc_slcsp: 4483.7 diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py index bdd10aa10ac..118e4f16533 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi.py @@ -15,20 +15,23 @@ class aca_magi(Variable): "Household income for the premium tax credit (IRC 36B(d)(2), 26 CFR " "1.36B-1(e)(1)): the taxpayer's modified AGI, both spouses' on a " "joint return, plus that of other members of the tax family. It is " - "medicaid_magi without the tax-exempt interest and Social Security " - "of the return's dependents who are outside the tax family " - "(is_aca_tax_family_member), which happens only on a return whose " - "head or spouse can be claimed elsewhere. medicaid_magi adds those " - "two amounts for every member (tax unit AGI already leaves " - "dependents' income out); both are nonnegative, so removing them " - "after medicaid_magi's zero floor gives the same result as removing " - "them before it." + "medicaid_magi's AGI plus MAGI additions, without the tax-exempt " + "interest and Social Security of the return's dependents who are " + "outside the tax family (is_aca_tax_family_member), floored at zero " + "once. Those amounts are left out only on a return whose head or " + "spouse can be claimed elsewhere (or, in a tax unit with neither, " + "whose oldest member can be); otherwise this equals medicaid_magi. " + "Tax unit AGI already leaves dependents' income out." ) def formula(tax_unit, period, parameters): person = tax_unit.members filer = person("is_tax_unit_head_or_spouse", period) - outside_family = ~filer & ~person("is_aca_tax_family_member", period) + has_filer = person.tax_unit.any(filer) + age = person("age", period) + oldest = person.get_rank(tax_unit, -age) == 0 + files_return = where(has_filer, filer, oldest) + outside_family = ~files_return & ~person("is_aca_tax_family_member", period) excluded = tax_unit.sum( outside_family * ( @@ -36,4 +39,10 @@ def formula(tax_unit, period, parameters): + person("social_security", period) ) ) - return max_(0, tax_unit("medicaid_magi", period) - excluded) + # medicaid_magi's AGI plus additions, before its zero floor, so a + # negative excluded amount is removed before the floor too. + additions = parameters(period).gov.hhs.medicaid.income.modification + unfloored = tax_unit("adjusted_gross_income", period) + add( + tax_unit, period, additions + ) + return max_(0, unfloored - excluded) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py index 389b06a6a8c..68907d3f4e4 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_coverage_family_member.py @@ -8,15 +8,15 @@ class is_aca_coverage_family_member(Variable): definition_period = YEAR reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#b_2" documentation = ( - "A Marketplace enrollee (pays_aca_premium) who is in the premium tax " - "credit tax family. 26 CFR 1.36B-3(b)(2) defines the coverage family " - "as the members of the taxpayer's family for whom the month is a " - "coverage month, and the benchmark plan in 1.36B-3(f) is the one " - "offered to the coverage family. An enrollee who can be claimed on " - "another return buys a plan but is not in it." + "A Marketplace enrollee (is_aca_marketplace_enrollee) who is in the " + "premium tax credit tax family. 26 CFR 1.36B-3(b)(2) defines the " + "coverage family as the members of the taxpayer's family for whom the " + "month is a coverage month, and the benchmark plan in 1.36B-3(f) is " + "the one offered to the coverage family. An enrollee who can be " + "claimed on another return buys a plan but is not in it." ) def formula(person, period, parameters): - return person("pays_aca_premium", period) & person( + return person("is_aca_marketplace_enrollee", period) & person( "is_aca_tax_family_member", period ) diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py new file mode 100644 index 00000000000..022a6059d4d --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py @@ -0,0 +1,56 @@ +from policyengine_us.model_api import * + + +class is_aca_marketplace_enrollee(Variable): + value_type = bool + entity = Person + label = "Person is enrolled in Marketplace coverage" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/cfr/text/45/155.305#a", + "https://www.law.cornell.edu/uscode/text/26/36B#c_1", + ) + documentation = ( + "Whether a person is enrolled in a Marketplace plan: the TIN, " + "immigration-status, other-minimum-essential-coverage and Medicaid " + "coverage-gap conditions of pays_aca_premium, without its premium " + "condition. A child beyond the three oldest is enrolled but pays no " + "age-rated premium (45 CFR 147.102(c)(1)); pays_aca_premium is this " + "variable restricted to those who pay one." + ) + + def formula(person, period, parameters): + immigration_eligible = person("is_aca_ptc_immigration_status_eligible", period) + taxpayer_has_tin = person.tax_unit("taxpayer_has_tin", period) + is_status_eligible = taxpayer_has_tin & immigration_eligible + + p = parameters(period).gov.aca + is_coverage_eligible = add(person, period, p.ineligible_coverage) == 0 + + # The Medicaid coverage gap: under the premium tax credit's income + # floor, income-ineligible under the eligibility scale itself, with no + # Medicaid pathway (already required by ineligible_coverage above: a + # non-expansion state, or a status bar), and without the below-poverty + # exception for lawfully present immigrants, which 26 U.S.C. + # 36B(c)(1)(B) carried until P.L. 119-21 repealed it from 2026. The + # scale's own result is read, not just its threshold, so a reform that + # makes the first bracket income-eligible reopens the credit here + # instead of being blocked by a threshold that did not move. The model + # assumes nobody in that gap buys the full-price benchmark plan; + # charging it made a Wyoming adult at $0 of income pay $11,769 a year + # (policyengine-us #9472). Only the floor is gated: above the + # eligibility ceiling, where the 2026 subsidy cliff returns, full + # price is what an enrollee pays. + income_eligibility = p.ptc_income_eligibility + magi_frac = person.tax_unit("aca_magi_fraction", period) + floor = income_eligibility.thresholds[1] + below_fpl_exception = person.tax_unit( + "aca_ptc_below_fpl_immigration_exception", period + ) + in_coverage_gap = ( + (magi_frac < floor) + & ~income_eligibility.calc(magi_frac) + & ~below_fpl_exception + ) + + return is_status_eligible & is_coverage_eligible & ~in_coverage_gap diff --git a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py index cac4db03b6c..48294317543 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_tax_family_member.py @@ -28,8 +28,11 @@ class is_aca_tax_family_member(Variable): "Dependent Taxpayer Test). No credit is allowed to someone another " "taxpayer can claim (IRC 36B(c)(1)(D)), so a return whose every head " "and spouse can be claimed has no applicable taxpayer and no family " - "for the credit. A tax unit with neither a head nor a spouse has no " - "modeled filer, so each member's own claimability decides. A " + "for the credit. A tax unit with neither a head nor a spouse (role " + "inference makes the oldest adult the head, so a unit of minors has " + "none) is treated as filed by its oldest member: everyone is in the " + "family if that member cannot be claimed elsewhere, and nobody is if " + "they can. A " "dependent on a return with an applicable taxpayer is a member even if " "another taxpayer could also claim them: this return claims them." ) @@ -45,4 +48,12 @@ def formula(person, period, parameters): "head_or_spouse_is_dependent_elsewhere_without_filing_exception", period ) dependent_member = applicable_taxpayer & claims_dependents - return where(has_filer, where(filer, ~claimable, dependent_member), ~claimable) + # No head or spouse: the oldest member files. + age = person("age", period) + oldest = person.get_rank(person.tax_unit, -age) == 0 + oldest_claimable = person.tax_unit.any(oldest & claimable) + return where( + has_filer, + where(filer, ~claimable, dependent_member), + ~oldest_claimable, + ) diff --git a/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py b/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py index 75c148f4766..044d4f01591 100644 --- a/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py +++ b/policyengine_us/variables/gov/aca/eligibility/pays_aca_premium.py @@ -24,49 +24,16 @@ class pays_aca_premium(Variable): "does, because the unsubsidized benchmark premium exceeds their " "income (policyengine-us #9472). A reader that wants the status and " "coverage conditions without that assumption should not reuse this " - "variable." + "variable. It is is_aca_marketplace_enrollee restricted to people " + "charged an age-rated premium: adults, and the three oldest children " + "in the tax unit." ) def formula(person, period, parameters): - immigration_eligible = person("is_aca_ptc_immigration_status_eligible", period) - taxpayer_has_tin = person.tax_unit("taxpayer_has_tin", period) - is_status_eligible = taxpayer_has_tin & immigration_eligible - + # An enrollee pays an age-rated premium unless they are a child + # beyond the three oldest (45 CFR 147.102(c)(1)). + enrollee = person("is_aca_marketplace_enrollee", period) p = parameters(period).gov.aca - is_coverage_eligible = add(person, period, p.ineligible_coverage) == 0 is_aca_adult = person("age", period) > p.slcsp.max_child_age child_pays = person("aca_child_index", period) <= p.max_child_count - pays_age_based_premium = is_aca_adult | child_pays - - # The Medicaid coverage gap: under the premium tax credit's income - # floor, income-ineligible under the eligibility scale itself, with no - # Medicaid pathway (already required by ineligible_coverage above: a - # non-expansion state, or a status bar), and without the below-poverty - # exception for lawfully present immigrants, which 26 U.S.C. - # 36B(c)(1)(B) carried until P.L. 119-21 repealed it from 2026. The - # scale's own result is read, not just its threshold, so a reform that - # makes the first bracket income-eligible reopens the credit here - # instead of being blocked by a threshold that did not move. The model - # assumes nobody in that gap buys the full-price benchmark plan; - # charging it made a Wyoming adult at $0 of income pay $11,769 a year - # (policyengine-us #9472). Only the floor is gated: above the - # eligibility ceiling, where the 2026 subsidy cliff returns, full - # price is what an enrollee pays. - income_eligibility = p.ptc_income_eligibility - magi_frac = person.tax_unit("aca_magi_fraction", period) - floor = income_eligibility.thresholds[1] - below_fpl_exception = person.tax_unit( - "aca_ptc_below_fpl_immigration_exception", period - ) - in_coverage_gap = ( - (magi_frac < floor) - & ~income_eligibility.calc(magi_frac) - & ~below_fpl_exception - ) - - return ( - is_status_eligible - & is_coverage_eligible - & pays_age_based_premium - & ~in_coverage_gap - ) + return enrollee & (is_aca_adult | child_pays) diff --git a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py index d06c3482cf2..d9c00f4f054 100644 --- a/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py +++ b/policyengine_us/variables/gov/aca/ptc/aca_ptc_coverage_family_premium.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.aca_ptc_slcsp import ( + benchmark_premium, +) class aca_ptc_coverage_family_premium(Variable): @@ -10,24 +13,32 @@ class aca_ptc_coverage_family_premium(Variable): reference = "https://www.law.cornell.edu/cfr/text/26/1.36B-3#d_1" documentation = ( "The enrollment premiums for the premium tax credit's lesser-of test " - "(26 CFR 1.36B-3(d)(1)(i)): the selected plan premium for the tax " - "unit, of which the coverage family's share is its benchmark " - "(aca_ptc_slcsp) over the whole enrollment's benchmark (slcsp). 26 " - "CFR 1.36B-3(h)(1) allocates a policy covering more than one family " - "'in proportion to the premiums for each taxpayer's applicable " - "benchmark plan'; the whole-enrollment benchmark stands in for the " - "sum of those benchmarks. The share is 1 whenever every enrollee is " - "in the coverage family. It applies to bronze plans too." + "(26 CFR 1.36B-3(d)(1)(i)). The model prices one policy per tax unit. " + "When it also covers enrollees outside the tax family, 26 CFR " + "1.36B-3(h)(1) allocates the premium 'in proportion to the premiums " + "for each taxpayer's applicable benchmark plan': the coverage family's " + "benchmark (aca_ptc_slcsp) over that plus the benchmark of the other " + "enrollees, priced as one family of their own. The whole premium is " + "the coverage family's when every enrollee is in it, and none is when " + "no enrollee is. It applies to bronze plans too." ) def formula(tax_unit, period, parameters): premium = tax_unit("selected_marketplace_plan_premium_proxy", period) - benchmark = add(tax_unit, period, ["slcsp"]) + person = tax_unit.members + enrolled = person("is_aca_marketplace_enrollee", period) + member = person("is_aca_tax_family_member", period) + every_enrollee_covered = ~tax_unit.any(enrolled & ~member) coverage_family_benchmark = add(tax_unit, period, ["aca_ptc_slcsp"]) + other_benchmark = sum( + benchmark_premium(tax_unit, month, parameters, ~member, enrolled) + for month in period.get_subperiods(MONTH) + ) + total = coverage_family_benchmark + other_benchmark share = np.divide( coverage_family_benchmark, - benchmark, - out=np.ones_like(benchmark), - where=benchmark > 0, + total, + out=np.zeros_like(total), + where=total > 0, ) - return premium * share + return premium * where(every_enrollee_covered, 1, share) diff --git a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py index ea2eaf3ae9a..a49a4df22a1 100644 --- a/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py +++ b/policyengine_us/variables/gov/aca/slspc/aca_ptc_slcsp.py @@ -1,4 +1,42 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( + family_tier_category, +) +from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_multiplier import ( + family_tier_multiplier, +) + + +def benchmark_premium(tax_unit, period, parameters, group, enrolled): + """Monthly second-lowest-cost silver premium for the enrollees of one + family on the return. + + `group` marks the family's members and `enrolled` the Marketplace + enrollees (person-level boolean arrays). Children are ranked within the + family, so only its three oldest children add an age-rated premium (45 CFR + 147.102(c)(1)); in family-tier states the tier is the family's own. + """ + person = tax_unit.members + p = parameters(period).gov.aca + age = person("age", period.this_year) + is_child = age <= p.slcsp.max_child_age + child_rank = person.get_rank(tax_unit, -age, condition=group & is_child) + charged = group & enrolled & (~is_child | (child_rank < p.max_child_count)) + base = person.household("slcsp_age_0", period) + multiplier = person("slcsp_age_curve_multiplier", period) + age_curve_applies = tax_unit("slcsp_age_curve_applies", period) + age_curve = tax_unit.sum(charged * base * multiplier) * age_curve_applies + tier_enrolled = group & enrolled + category = family_tier_category(tax_unit, period, parameters, tier_enrolled) + tier_multiplier = family_tier_multiplier( + tax_unit, period, parameters, category, tier_enrolled + ) + family_tier = where( + tax_unit("slcsp_family_tier_applies", period), + tax_unit.household("slcsp_age_0", period) * tier_multiplier, + 0, + ) + return age_curve + family_tier class aca_ptc_slcsp(Variable): @@ -13,28 +51,25 @@ class aca_ptc_slcsp(Variable): ) documentation = ( "The second-lowest-cost silver plan premium offered to the coverage " - "family (26 CFR 1.36B-3(f)): the enrollees who are in the premium tax " - "credit tax family. When every enrollee on the return is in it, this " - "is slcsp itself, read directly so a supplied slcsp carries through. " - "When an enrollee is not (one who can be claimed on another return, " - "or a dependent of a return with such a filer), it is the age-curve " - "premiums of the coverage family members or, where family tiers " - "apply, the tier premium for them. slcsp keeps every enrollee, since " - "everyone still buys a plan." + "family (26 CFR 1.36B-3(f)): the Marketplace enrollees who are in the " + "premium tax credit tax family. When every enrollee on the return is " + "in it, this is slcsp itself, read directly so a supplied slcsp " + "carries through. When an enrollee is not (one who can be claimed on " + "another return, or a dependent of a return with such a filer), the " + "coverage family is priced on its own: age-curve premiums with its " + "children ranked among themselves, so its three oldest children are " + "charged, or, where family tiers apply, its own tier. slcsp keeps " + "every enrollee, since everyone still buys a plan." ) def formula(tax_unit, period, parameters): person = tax_unit.members - enrolled = person("pays_aca_premium", period.this_year) - covered = person("is_aca_coverage_family_member", period.this_year) - every_enrollee_covered = ~tax_unit.any(enrolled & ~covered) - age_curve = tax_unit.sum( - covered * person("slcsp_age_curve_amount_person", period) - ) - family_tier = tax_unit.household("slcsp_age_0", period) * tax_unit( - "aca_ptc_slcsp_family_tier_multiplier", period + enrolled = person("is_aca_marketplace_enrollee", period.this_year) + member = person("is_aca_tax_family_member", period.this_year) + every_enrollee_covered = ~tax_unit.any(enrolled & ~member) + coverage_family_benchmark = benchmark_premium( + tax_unit, period, parameters, member, enrolled ) - coverage_family_benchmark = age_curve + family_tier return where( every_enrollee_covered, tax_unit("slcsp", period), diff --git a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py index 89231d0249b..82b44cf6cee 100644 --- a/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py +++ b/policyengine_us/variables/gov/aca/slspc/is_aca_family_tier_dependent_child.py @@ -1,4 +1,21 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.is_aca_ny_age_29_dependent_child import ( + ny_age_29_child_condition, +) + + +def family_tier_child_condition(person, period, parameters): + """The family-tier dependent-child test without the premium condition.""" + p = parameters(period).gov.aca + age = person("monthly_age", period) + is_tax_dependent = person("is_tax_unit_dependent", period) + under_child_only_age = age <= p.slcsp.max_child_age + under_age_threshold = age < p.family_tier_dependent_child_age_threshold + return ( + under_child_only_age + | (is_tax_dependent & under_age_threshold) + | ny_age_29_child_condition(person, period, parameters) + ) class is_aca_family_tier_dependent_child(Variable): @@ -8,14 +25,6 @@ class is_aca_family_tier_dependent_child(Variable): definition_period = MONTH def formula(person, period, parameters): - p = parameters(period).gov.aca - age = person("monthly_age", period) - is_tax_dependent = person("is_tax_unit_dependent", period) - under_child_only_age = age <= p.slcsp.max_child_age - under_age_threshold = age < p.family_tier_dependent_child_age_threshold - ny_age_29_child = person("is_aca_ny_age_29_dependent_child", period) - return person("pays_aca_premium", period) & ( - under_child_only_age - | (is_tax_dependent & under_age_threshold) - | ny_age_29_child + return person("pays_aca_premium", period) & family_tier_child_condition( + person, period, parameters ) diff --git a/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py b/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py index cbd16a30ef5..2f925c9b6b0 100644 --- a/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py +++ b/policyengine_us/variables/gov/aca/slspc/is_aca_ny_age_29_dependent_child.py @@ -1,6 +1,19 @@ from policyengine_us.model_api import * +def ny_age_29_child_condition(person, period, parameters): + """New York's age-29 dependent-child test without the premium condition.""" + p = parameters(period).gov.aca + state_code = person.household("state_code", period) + in_ny = state_code == state_code.possible_values.NY + age = person("monthly_age", period) + lower_age = p.family_tier_dependent_child_age_threshold + upper_age = p.ny_age_29_dependent_child_age_threshold + in_age_range = (age >= lower_age) & (age < upper_age) + is_tax_dependent = person("is_tax_unit_dependent", period) + return in_ny & in_age_range & is_tax_dependent + + class is_aca_ny_age_29_dependent_child(Variable): value_type = bool entity = Person @@ -8,14 +21,6 @@ class is_aca_ny_age_29_dependent_child(Variable): definition_period = MONTH def formula(person, period, parameters): - p = parameters(period).gov.aca - state_code = person.household("state_code", period) - in_ny = state_code == state_code.possible_values.NY - age = person("monthly_age", period) - lower_age = p.family_tier_dependent_child_age_threshold - upper_age = p.ny_age_29_dependent_child_age_threshold - in_age_range = (age >= lower_age) & (age < upper_age) - is_tax_dependent = person("is_tax_unit_dependent", period) - return ( - person("pays_aca_premium", period) & in_ny & in_age_range & is_tax_dependent + return person("pays_aca_premium", period) & ny_age_29_child_condition( + person, period, parameters ) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py index 1b9a354fdd4..a61cc5b803e 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_category.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.is_aca_family_tier_dependent_child import ( + family_tier_child_condition, +) class FamilyTierCategory(Enum): @@ -27,7 +30,7 @@ def family_tier_category(tax_unit, period, parameters, enrolled): state_code = tax_unit.household("state_code_str", period) person = tax_unit.members - dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + dependent_child = enrolled & family_tier_child_condition(person, period, parameters) non_dependent_adult_count = tax_unit.sum(enrolled & ~dependent_child) anchored_child_count = tax_unit.sum(dependent_child) diff --git a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py index 194ac280ae0..04227da99d9 100644 --- a/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py +++ b/policyengine_us/variables/gov/aca/slspc/slcsp_family_tier_multiplier.py @@ -1,4 +1,10 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.slspc.is_aca_family_tier_dependent_child import ( + family_tier_child_condition, +) +from policyengine_us.variables.gov.aca.slspc.is_aca_ny_age_29_dependent_child import ( + ny_age_29_child_condition, +) from policyengine_us.variables.gov.aca.slspc.slcsp_family_tier_category import ( FamilyTierCategory, ) @@ -46,10 +52,10 @@ def family_tier_multiplier(tax_unit, period, parameters, family_category, enroll ) person = tax_unit.members - dependent_child = enrolled & person("is_aca_family_tier_dependent_child", period) + dependent_child = enrolled & family_tier_child_condition(person, period, parameters) adult_count = tax_unit.sum(enrolled & ~dependent_child) extra_adults = max_(adult_count - 2, 0) - age_29_child = enrolled & person("is_aca_ny_age_29_dependent_child", period) + age_29_child = enrolled & ny_age_29_child_condition(person, period, parameters) age_29_multiplier = where( tax_unit.any(age_29_child), parameters(period).gov.aca.ny_age_29_dependent_child_tier_multiplier,