From 2e10137871c8236c41cb6618a124e490d5a1bec0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 16:31:54 -0400 Subject: [PATCH] Treat income exactly at 400% FPL as premium tax credit eligible 26 U.S.C. 36B(c)(1)(A) covers household income that "does not exceed 400 percent" of the poverty line, and Form 8962 Worksheet 2 compares income with four times the poverty line in dollars before truncating the percentage. The income scale's 400% bracket starts at 4.00, so evaluating it on the truncated aca_magi_fraction excluded a household at exactly 400%. aca_ptc_income_eligible keeps such income on the eligible side and is read by is_aca_ptc_eligible and the Basic Health Program payment. Co-Authored-By: Claude Opus 5.5 --- .../aca-ptc-400-percent-inclusive.fixed.md | 1 + .../eligibility/aca_ptc_income_eligible.yaml | 157 ++++++++++++++++++ .../aca/eligibility/is_aca_ptc_eligible.yaml | 50 ++++++ .../policy/baseline/gov/aca/ptc/aca_ptc.yaml | 57 +++++++ ...test_aca_ptc_income_eligible_properties.py | 134 +++++++++++++++ .../eligibility/aca_ptc_income_eligible.py | 46 +++++ .../aca/eligibility/is_aca_ptc_eligible.py | 4 +- .../basic_health_program.py | 5 +- 8 files changed, 447 insertions(+), 7 deletions(-) create mode 100644 changelog.d/aca-ptc-400-percent-inclusive.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_income_eligible.yaml create mode 100644 policyengine_us/tests/unit/test_aca_ptc_income_eligible_properties.py create mode 100644 policyengine_us/variables/gov/aca/eligibility/aca_ptc_income_eligible.py diff --git a/changelog.d/aca-ptc-400-percent-inclusive.fixed.md b/changelog.d/aca-ptc-400-percent-inclusive.fixed.md new file mode 100644 index 00000000000..2d45df9b9f2 --- /dev/null +++ b/changelog.d/aca-ptc-400-percent-inclusive.fixed.md @@ -0,0 +1 @@ +Treat household income exactly at 400% of the poverty line as eligible for the premium tax credit in 2026 and later. 26 U.S.C. 36B(c)(1)(A) covers income that "does not exceed 400 percent", and Form 8962 Worksheet 2 decides "more than 400%" on the dollar amounts before truncating the percentage; the income scale's 400% bracket started at 4.00, so the truncated fraction excluded a household at exactly 400%. The new `aca_ptc_income_eligible` variable carries the income test for `is_aca_ptc_eligible` and the Basic Health Program payment. diff --git a/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_income_eligible.yaml new file mode 100644 index 00000000000..6ae61ec52a0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/eligibility/aca_ptc_income_eligible.yaml @@ -0,0 +1,157 @@ +# 26 U.S.C. 36B(c)(1)(A): household income that "equals or exceeds 100 percent +# but does not exceed 400 percent" of the poverty line. Form 8962 Worksheet 2 +# decides "more than 400%" on the dollar amounts, then drops the decimals of the +# percentage for line 5. Income-derived cases use a single filer in Texas and +# the prior-year one-person poverty guideline: $15,650 (2025) for tax year 2026 +# and $15,960 (2026) for tax year 2027. 400% of those is $62,600 and $63,840. + +- name: Case 1, 2026, $15,649 (99.994% of the 2025 guideline) is below the floor. + period: 2026 + input: + aca_magi: 15_649 + state_code: TX + output: + aca_magi_fraction: 0.99 + aca_ptc_income_eligible: false + +- name: Case 2, 2026, exactly 100% of the 2025 guideline meets the floor. + period: 2026 + input: + aca_magi: 15_650 + state_code: TX + output: + aca_magi_fraction: 1.00 + aca_ptc_income_eligible: true + +- name: Case 3, 2026, $62,599 (399.994%) is eligible. + period: 2026 + input: + aca_magi: 62_599 + state_code: TX + output: + aca_magi_fraction: 3.99 + aca_ptc_income_eligible: true + +- name: Case 4, 2026, exactly 400% does not exceed 400% and is eligible. + # Worksheet 2: $62,600 is not more than 4 x $15,650, so line 5 is 400 and the + # credit is allowed. The scale's 400% bracket starts at 4.00, so main excluded + # this household. + period: 2026 + input: + aca_magi: 62_600 + state_code: TX + output: + aca_magi_fraction: 4.00 + aca_ptc_income_eligible: true + +- name: Case 5, 2026, $62,600.47 (400.003%) exceeds 400% and is ineligible. + # The truncated fraction is still 4.00; the dollar comparison decides. + period: 2026 + input: + aca_magi: 62_600.47 + state_code: TX + output: + aca_magi_fraction: 4.00 + aca_ptc_income_eligible: false + +- name: Case 6, 2026, $62,755.56 (400.994%) is ineligible. + period: 2026 + input: + aca_magi: 62_755.56 + state_code: TX + output: + aca_magi_fraction: 4.00 + aca_ptc_income_eligible: false + +- name: Case 7, 2026, $62,756.50 (401%) is ineligible. + period: 2026 + input: + aca_magi: 62_756.50 + state_code: TX + output: + aca_magi_fraction: 4.01 + aca_ptc_income_eligible: false + +- name: Case 8, 2027, exactly 400% of the 2026 guideline is eligible. + period: 2027 + input: + aca_magi: 63_840 + state_code: TX + output: + aca_magi_fraction: 4.00 + aca_ptc_income_eligible: true + +- name: Case 9, 2027, $63,840.48 (400.003%) is ineligible. + period: 2027 + input: + aca_magi: 63_840.48 + state_code: TX + output: + aca_magi_fraction: 4.00 + aca_ptc_income_eligible: false + +- name: Case 10, 2025, above 400% is eligible while the enhanced credit removes the cap. + # 2024 one-person guideline $15,060; $60,240.45 is 400.003%. + period: 2025 + input: + aca_magi: 60_240.45 + state_code: TX + output: + aca_magi_fraction: 4.00 + aca_ptc_income_eligible: true + +- name: Case 11, 2026, a two-person unit at exactly 400% of its guideline is eligible. + # 2025 two-person guideline $21,150; 4 x $21,150 = $84,600. + period: 2026 + input: + people: + head: + age: 40 + spouse: + age: 40 + tax_units: + tax_unit: + members: [head, spouse] + aca_magi: 84_600 + households: + household: + members: [head, spouse] + state_code: TX + output: + aca_ptc_income_eligible: true + +- name: Case 12, 2026, the same two-person unit $1 above 400% is ineligible. + period: 2026 + input: + people: + head: + age: 40 + spouse: + age: 40 + tax_units: + tax_unit: + members: [head, spouse] + aca_magi: 84_601 + households: + household: + members: [head, spouse] + state_code: TX + output: + aca_ptc_income_eligible: false + +- name: Case 13, 2026, a supplied fraction of 3.99 without income is eligible. + # A supplied fraction alone stands for a whole percentage. + period: 2026 + input: + aca_magi_fraction: 3.99 + state_code: TX + output: + aca_ptc_income_eligible: true + +- name: Case 14, 2026, a supplied fraction of 4.01 without income is ineligible. + period: 2026 + input: + aca_magi_fraction: 4.01 + state_code: TX + output: + aca_ptc_income_eligible: 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..18d68f0cfc3 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,53 @@ has_qualifying_non_marketplace_health_coverage_at_interview: [true] pays_aca_premium: [false] is_aca_ptc_eligible: [false] + +- name: Exactly 400% of the 2025 poverty guideline is eligible in 2026 + # 26 U.S.C. 36B(c)(1)(A): income that "does not exceed 400 percent" of the + # poverty line; 4 x $15,650 = $62,600. + period: 2026 + input: + people: + person1: + age: 40 + is_medicaid_eligible: false + is_chip_eligible: false + is_basic_health_program_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 62_600 + filing_status: SINGLE + households: + household: + members: [person1] + state_code: TX + output: + aca_ptc_income_eligible: true + is_aca_ptc_eligible: [true] + +- name: Income 400.003% of the 2025 poverty guideline is ineligible in 2026 + period: 2026 + input: + people: + person1: + age: 40 + is_medicaid_eligible: false + is_chip_eligible: false + is_basic_health_program_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 62_600.47 + filing_status: SINGLE + households: + household: + members: [person1] + state_code: TX + output: + aca_ptc_income_eligible: false + is_aca_ptc_eligible: [false] diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml index 2214672a9e5..294efaa3d37 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml @@ -276,3 +276,60 @@ output: is_aca_ptc_eligible: [false] aca_ptc: 0 + +- name: ACA PTC is paid at exactly 400% of the poverty line in 2026 + # 4 x $15,650 (2025 one-person guideline) = $62,600 "does not exceed 400 + # percent" (26 U.S.C. 36B(c)(1)(A)). Form 8962 line 5 is 400, so the + # applicable percentage is the 2026 maximum of 9.96%. + # PTC = 12_000 - 0.0996 * 62_600 = 12_000 - 6_234.96 = 5_765.04. + absolute_error_margin: 0.01 + period: 2026 + input: + people: + person1: + age: 40 + is_medicaid_eligible: false + is_chip_eligible: false + is_basic_health_program_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 62_600 + slcsp: 12_000 + tax_unit_is_filer: true + filing_status: SINGLE + households: + household: + members: [person1] + state_code: TX + output: + aca_required_contribution_percentage: 0.0996 + aca_ptc: 5_765.04 + +- name: ACA PTC is zero at 400.003% of the poverty line in 2026 + absolute_error_margin: 0.01 + period: 2026 + input: + people: + person1: + age: 40 + is_medicaid_eligible: false + is_chip_eligible: false + is_basic_health_program_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 62_600.47 + slcsp: 12_000 + tax_unit_is_filer: true + filing_status: SINGLE + households: + household: + members: [person1] + state_code: TX + output: + aca_ptc: 0 diff --git a/policyengine_us/tests/unit/test_aca_ptc_income_eligible_properties.py b/policyengine_us/tests/unit/test_aca_ptc_income_eligible_properties.py new file mode 100644 index 00000000000..8ba323fe831 --- /dev/null +++ b/policyengine_us/tests/unit/test_aca_ptc_income_eligible_properties.py @@ -0,0 +1,134 @@ +"""Properties of the premium tax credit income test. + +26 U.S.C. 36B(c)(1)(A) makes an applicable taxpayer one whose household +income "equals or exceeds 100 percent but does not exceed 400 percent" of the +poverty line. 36B(c)(1)(E) removed the 400% ceiling for 2021 through 2025. +The bands below are transcribed from the statute, not read from the +parameters, and are evaluated in exact rational arithmetic on the values the +model receives (ACA MAGI and the guideline are stored as float32). + +aca_magi_fraction drops the decimals of the percentage, as Form 8962 line 5 +does, so every income from 400.00% to 400.99% of the guideline becomes 4.00. +Form 8962 Worksheet 2 first decides "more than 400%" on the dollar amounts. + +- The income scale's thresholds and amounts match the statute. +- For any income and guideline, aca_ptc_income_eligible equals the exact + reading of 36B(c)(1)(A). Incomes are whole dollars or cents near an edge, + so income exactly at 100% and 400% of the guideline is hit. +""" + +from fractions import Fraction + +import numpy as np +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +# Tax year: (floor, ceiling or None when 36B(c)(1)(E) removes it). +STATUTE_BANDS = { + 2025: (Fraction(1), None), + 2026: (Fraction(1), Fraction(4)), + 2027: (Fraction(1), Fraction(4)), +} +EDGES = (Fraction(1), Fraction(4)) +TAX_UNITS = 64 + + +def as_received(value): + """The exact value of a number after float32 storage.""" + return Fraction(float(np.float32(value))) + + +def statute_eligible(year, income, guideline): + floor, ceiling = STATUTE_BANDS[year] + ratio = max(as_received(income), 0) / as_received(guideline) + return ratio >= floor and (ceiling is None or ratio <= ceiling) + + +@pytest.fixture(scope="module") +def simulations(): + sims = {} + for year in STATUTE_BANDS: + sims[year] = Simulation( + situation={ + "people": {f"p{i}": {"age": {year: 40}} for i in range(TAX_UNITS)}, + "tax_units": { + f"t{i}": {"members": [f"p{i}"]} for i in range(TAX_UNITS) + }, + "households": { + f"h{i}": {"members": [f"p{i}"], "state_code": {year: "TX"}} + for i in range(TAX_UNITS) + }, + } + ) + return sims + + +def model_eligible(sim, year, incomes, guidelines): + sim.set_input("aca_magi", year, np.asarray(incomes, dtype=float)) + sim.set_input("tax_unit_fpg", year - 1, np.asarray(guidelines, dtype=float)) + for variable in ("aca_magi_fraction", "aca_ptc_income_eligible"): + sim.delete_arrays(variable, year) + return sim.calculate("aca_ptc_income_eligible", year) + + +@st.composite +def tax_unit_incomes(draw): + """An income and guideline, often within $2 (in cents) of an edge.""" + guideline = draw( + st.one_of( + st.integers(10_000, 80_000), + st.integers(500, 4_000).map(lambda k: 20 * k), + ) + ) + edge = draw(st.sampled_from(EDGES)) + near_edge = (int(edge * guideline) * 100 + draw(st.integers(-200, 200))) / 100 + income = draw( + st.one_of( + st.just(near_edge), + st.integers(-50_000, 5 * guideline).map(float), + ) + ) + return income, guideline + + +@pytest.mark.parametrize("year", sorted(STATUTE_BANDS)) +def test_scale_matches_statute(year, simulations): + scale = ( + simulations[year] + .tax_benefit_system.parameters(f"{year}-01-01") + .gov.aca.ptc_income_eligibility + ) + floor, ceiling = STATUTE_BANDS[year] + assert [Fraction(str(t)) for t in scale.thresholds] == [0, floor, Fraction(4)] + assert [bool(a) for a in scale.amounts] == [False, True, ceiling is None] + + +@pytest.mark.parametrize("year", sorted(STATUTE_BANDS)) +@settings(max_examples=60, deadline=None, derandomize=True) +@given(units=st.lists(tax_unit_incomes(), min_size=1, max_size=TAX_UNITS)) +# 2025 one-person guideline $15,650: 99.994%, 100%, 399.994%, exactly 400%, +# 400.003% and 400.994%. Truncation maps the last three to 4.00. +@example( + units=[ + (15_649.0, 15_650), + (15_650.0, 15_650), + (62_599.0, 15_650), + (62_600.0, 15_650), + (62_600.47, 15_650), + (62_755.56, 15_650), + ] +) +def test_income_test_matches_statute(year, simulations, units): + padded = (units * TAX_UNITS)[:TAX_UNITS] + incomes, guidelines = zip(*padded) + expected = [statute_eligible(year, income, g) for income, g in padded] + actual = model_eligible(simulations[year], year, incomes, guidelines) + mismatches = [ + (income, g, bool(a)) + for income, g, a, e in zip(incomes, guidelines, actual, expected) + if bool(a) != e + ] + assert not mismatches diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_ptc_income_eligible.py b/policyengine_us/variables/gov/aca/eligibility/aca_ptc_income_eligible.py new file mode 100644 index 00000000000..f969e138f5c --- /dev/null +++ b/policyengine_us/variables/gov/aca/eligibility/aca_ptc_income_eligible.py @@ -0,0 +1,46 @@ +from policyengine_us.model_api import * + + +class aca_ptc_income_eligible(Variable): + value_type = bool + entity = TaxUnit + label = "Household income meets the ACA premium tax credit income test" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/36B#c_1_A", + # Worksheet 2 (line 5): compare household income with 400% of the + # poverty line in dollars, then truncate the percentage. + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=9", + ) + documentation = ( + "Whether household income passes the premium tax credit income " + "scale (gov.aca.ptc_income_eligibility), not counting the " + "below-poverty exception for lawfully present immigrants. 26 U.S.C. " + "36B(c)(1)(A) covers income that 'equals or exceeds 100 percent but " + "does not exceed 400 percent' of the poverty line, so both edges are " + "inclusive. Form 8962 Worksheet 2 first asks whether household income " + "is more than four times the poverty line, in dollars, and only then " + "drops the decimals of the percentage for line 5. The scale's " + "brackets start at their thresholds, so evaluating it on the " + "truncated aca_magi_fraction alone excluded income exactly at 400% of " + "the poverty line. A truncated fraction equal to a threshold is " + "exactly at it only when truncation dropped nothing; such income " + "keeps the eligible side of the boundary." + ) + + def formula(tax_unit, period, parameters): + scale = parameters(period).gov.aca.ptc_income_eligibility + # Form 8962 line 5: the percentage with decimals dropped. + magi_frac = tax_unit("aca_magi_fraction", period) + # Worksheet 2 compares the dollar amounts, so income exactly at a + # threshold is distinguished from income up to one point above it. + magi = max_(tax_unit("aca_magi", period), 0) + exact_magi_frac = magi / tax_unit("tax_unit_fpg", period.last_year) + at_whole_percentage = exact_magi_frac <= magi_frac + # The scale's brackets start at their thresholds, which is right for + # the 100% floor ("equals or exceeds"). At a ceiling ("does not + # exceed"), income exactly at the threshold stays in the bracket + # below it, which a right-closed evaluation returns. + return scale.calc(magi_frac) | ( + at_whole_percentage & scale.calc(magi_frac, right=True) + ) 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..6c209cecc48 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_ptc_eligible.py @@ -12,9 +12,7 @@ def formula(person, period, parameters): separate = fstatus == fstatus.possible_values.SEPARATE # determine income eligibility for ACA PTC - p = parameters(period).gov.aca - magi_frac = person.tax_unit("aca_magi_fraction", period) - standard_income_eligible = p.ptc_income_eligibility.calc(magi_frac) + standard_income_eligible = person.tax_unit("aca_ptc_income_eligible", period) below_fpl_exception = person.tax_unit( "aca_ptc_below_fpl_immigration_exception", period ) diff --git a/policyengine_us/variables/gov/hhs/basic_health_program/basic_health_program.py b/policyengine_us/variables/gov/hhs/basic_health_program/basic_health_program.py index 45ce3edad91..7fc59db6331 100644 --- a/policyengine_us/variables/gov/hhs/basic_health_program/basic_health_program.py +++ b/policyengine_us/variables/gov/hhs/basic_health_program/basic_health_program.py @@ -27,10 +27,7 @@ def formula(tax_unit, period, parameters): def formula_2026(tax_unit, period, parameters): p = parameters(period).gov.hhs.basic_health_program.payment - magi_fraction = tax_unit("aca_magi_fraction", period) - ptc_income_eligible = parameters(period).gov.aca.ptc_income_eligibility.calc( - magi_fraction - ) + ptc_income_eligible = tax_unit("aca_ptc_income_eligible", period) # The adjusted reference premium is a MONTH variable; reading it from # this YEAR formula sums the twelve monthly values into an annual # figure, matching the annual aca_magi contribution below (the same