diff --git a/changelog.d/aca-fpl-coverage-year-state.fixed.md b/changelog.d/aca-fpl-coverage-year-state.fixed.md new file mode 100644 index 00000000000..59cd5a45e93 --- /dev/null +++ b/changelog.d/aca-fpl-coverage-year-state.fixed.md @@ -0,0 +1 @@ +The premium tax credit's poverty line (aca_fpg) uses the state of residence in the coverage year, so Alaska and Hawaii households use their own guidelines when the state is supplied only for the coverage year. Households in Puerto Rico, Guam and the Virgin Islands, which have no poverty guideline and no Exchange, are not Marketplace enrollees, pay no Marketplace premium and are not eligible for the credit. 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 71e0d83e0bb..06d19911481 100644 --- a/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py +++ b/policyengine_us/tests/core/test_claimable_filer_ptc_invariants.py @@ -31,8 +31,8 @@ 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 + `aca_fpg` = the prior year's guideline for the coverage year's tax family + size and state group (Form 8962 lines 1 and 4). 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 @@ -50,6 +50,7 @@ from hypothesis import strategies as st from policyengine_us import Simulation +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg from policyengine_us.tests.core.test_dependent_elsewhere_head_spouse_swap_invariance import ( CLAIM_PATTERNS, TOLERANCE, @@ -343,9 +344,14 @@ def _identities(sim, year): np.testing.assert_array_equal( size[unclaimed], _calc(sim, "tax_unit_size", year)[unclaimed] ) + # Form 8962 lines 1 and 4: the prior year's guideline for the coverage + # year's tax family size and state of residence (26 CFR 1.36B-1(h)). + state_group = sim.populations["tax_unit"].household("state_group_str", str(year)) + expected_fpg = fpg( + size, state_group, str(year - 1), sim.tax_benefit_system.parameters + ) np.testing.assert_allclose( - _calc(sim, "aca_fpg", year)[unclaimed], - _calc(sim, "tax_unit_fpg", year - 1)[unclaimed], + _calc(sim, "aca_fpg", year)[unclaimed], expected_fpg[unclaimed] ) 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 0be43407ad3..f40f6970056 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 @@ -41,6 +41,194 @@ output: aca_magi_fraction: 2.54 +- name: Coverage in 2026 in Alaska uses Alaska's 2025 guideline. + # 30,000 / 19,550 = 1.5345, floored to 1.53. The contiguous states' + # guideline (15,650) would give 1.91. + period: 2026 + input: + people: + person1: + age: 40 + tax_units: + tax_unit: + members: [person1] + aca_magi: 30_000 + households: + household: + members: [person1] + state_code: AK + output: + aca_magi_fraction: 1.53 + +- name: Coverage in 2026 in Hawaii uses Hawaii's 2025 guideline. + # 30,000 / 17,990 = 1.6676, floored to 1.66. The contiguous states' + # guideline (15,650) would give 1.91. + period: 2026 + input: + people: + person1: + age: 40 + tax_units: + tax_unit: + members: [person1] + aca_magi: 30_000 + households: + household: + members: [person1] + state_code: HI + output: + aca_magi_fraction: 1.66 + +- name: Coverage in 2026 for an Alaska family of three uses Alaska's per-person increment. + # 19,550 + 2 x 6,880 = 33,310; 50,000 / 33,310 = 1.5011, floored to 1.50. + period: 2026 + input: + people: + parent: + age: 40 + child1: + age: 10 + child2: + age: 8 + tax_units: + tax_unit: + members: [parent, child1, child2] + aca_magi: 50_000 + households: + household: + members: [parent, child1, child2] + state_code: AK + output: + aca_magi_fraction: 1.5 + +- name: A household that moves from Alaska in 2025 to Texas in 2026 uses Texas's guideline for 2026 coverage. + # The state of residence in the coverage year decides the guideline, not + # the state in the guideline year. 30,000 / 15,650 = 1.9169, floored to + # 1.91. Alaska's 2025 guideline (19,550) would give 1.53. + period: 2026 + input: + people: + person1: + age: 40 + tax_units: + tax_unit: + members: [person1] + aca_magi: 30_000 + households: + household: + members: [person1] + state_code: + 2025: AK + 2026: TX + output: + aca_magi_fraction: 1.91 + +- name: Family size is the coverage year's. + # aca_tax_family_size is 3 for 2026 only; 2025 counts the one listed + # member. Alaska's 2025 guideline for three: 19,550 + 2 x 6,880 = 33,310. + # 50,000 / 33,310 = 1.5011, floored to 1.50. The 2025 size of one would + # give 50,000 / 19,550 = 2.55. + period: 2026 + input: + people: + person1: + age: 40 + tax_units: + tax_unit: + members: [person1] + aca_magi: 50_000 + aca_tax_family_size: + 2026: 3 + households: + household: + members: [person1] + state_code: AK + output: + aca_magi_fraction: 1.5 + +- name: Coverage in 2025 in Alaska uses Alaska's 2024 guideline. + # 30,000 / 18,810 = 1.5949, floored to 1.59. + period: 2025 + input: + people: + person1: + age: 40 + tax_units: + tax_unit: + members: [person1] + aca_magi: 30_000 + households: + household: + members: [person1] + state_code: AK + output: + aca_magi_fraction: 1.59 + +- name: Households in Alaska and Texas in one simulation each use their own state's guideline. + # 30,000 / 19,550 = 1.53 and 30,000 / 15,650 = 1.91. + period: 2026 + input: + people: + person1: + age: 40 + person2: + age: 40 + tax_units: + tax_unit1: + members: [person1] + aca_magi: 30_000 + tax_unit2: + members: [person2] + aca_magi: 30_000 + households: + household1: + members: [person1] + state_code: AK + household2: + members: [person2] + state_code: TX + output: + aca_magi_fraction: [1.53, 1.91] + +- name: Income exactly on a whole percentage of the guideline is not floored one point low. + # 100 x 36,621 / 15,650 = 234 exactly, so Worksheet 2 gives 234. Dividing + # first, in the 32-bit arithmetic aca_magi and aca_fpg are stored in, gives + # 36,621 / 15,650 x 100 just under 234, which floors to 2.33. + period: 2026 + input: + people: + person1: + age: 40 + tax_units: + tax_unit: + members: [person1] + aca_magi: 36_621 + households: + household: + members: [person1] + state_code: TX + output: + aca_magi_fraction: 2.34 + +- name: Negative ACA MAGI gives a fraction of 0. + # MAGI is floored at 0 before dividing (Form 8962 line 3 enters -0- for a + # negative total). From PR 10413's cases. + period: 2026 + input: + people: + person1: + age: 40 + tax_units: + tax_unit: + members: [person1] + aca_magi: -5_000 + households: + household: + members: [person1] + state_code: AK + output: + aca_magi_fraction: 0 + - name: 2025 joint return with one claimable spouse measures income against the one-person line (the claimable spouse is the head) period: 2025 input: 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 679889fde9b..e18b4d49c2d 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 @@ -856,3 +856,191 @@ output: is_medicare_eligible: false is_aca_ptc_eligible: true + +- name: 2026 Alaska single adult at $70,000 is under 400% of Alaska's guideline + # 70,000 / 19,550 (Alaska's 2025 guideline for one) = 3.58, inside the + # 1.00 to 4.00 range. The contiguous states' guideline (15,650) would give + # 4.47, above it. + period: 2026 + input: + people: + person1: + age: 30 + is_medicaid_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_chip_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 70_000 + households: + household: + members: [person1] + state_code: AK + output: + aca_magi_fraction: 3.58 + is_aca_ptc_eligible: true + +- name: 2026 Alaska single adult just under Alaska's guideline is below the 100% floor + # 19,549 / 19,550 = 0.99995, floored to 0.99, below the 1.00 floor; the + # below-poverty exception ends in 2026. The contiguous states' guideline + # (15,650) would give 1.24. + period: 2026 + input: + people: + person1: + age: 30 + is_medicaid_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_chip_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 19_549 + households: + household: + members: [person1] + state_code: AK + output: + aca_magi_fraction: 0.99 + is_aca_ptc_eligible: false + +- name: 2026 Alaska single adult just under 400% of Alaska's guideline is eligible + # 78,199 / 19,550 = 3.99995, floored to 3.99, inside the 1.00 to 4.00 + # range. The contiguous states' guideline (15,650) would give 4.99. + period: 2026 + input: + people: + person1: + age: 30 + is_medicaid_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_chip_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 78_199 + households: + household: + members: [person1] + state_code: AK + output: + aca_magi_fraction: 3.99 + is_aca_ptc_eligible: true + +- name: 2024 Puerto Rico single adult pays no Marketplace premium and is not eligible + # HHS defines no guideline for Puerto Rico, and no Exchange operates there + # (26 U.S.C. 36B(c)(2)(A); 42 U.S.C. 18024(d)), so the adult isn't a + # Marketplace enrollee and pays_aca_premium is false. Without the + # no-Exchange test, the infinite fraction (the + # placeholder guideline is 0) is outside the coverage gap and + # pays_aca_premium would be true. On main, with the state supplied for 2024 + # only, the 2023 guideline year fell back to the default state's guideline + # (30,000 / 14,580, floored to 2.05), so the household was eligible. + period: 2024 + input: + people: + person1: + age: 30 + is_medicaid_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_chip_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 30_000 + households: + household: + members: [person1] + state_code: PR + output: + is_aca_marketplace_enrollee: false + pays_aca_premium: false + is_aca_ptc_eligible: false + +- name: 2026 Puerto Rico single adult with no income has a required contribution of 0 + # The guideline is 0, so the fraction is infinite rather than 0 / 0. An + # infinite fraction falls in the top bracket, above the 400% limit, and + # 2026 has no income eligibility there, so the percentage is capped out + # to 0. Plain division would give NaN here, and a fraction of 0 would give + # the 2026 initial rate of 0.021. + period: 2026 + input: + people: + person1: + age: 30 + is_medicaid_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_chip_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 0 + households: + household: + members: [person1] + state_code: PR + output: + aca_required_contribution_percentage: 0 + is_aca_marketplace_enrollee: false + pays_aca_premium: false + is_aca_ptc_eligible: false + +- name: 2026 Guam single adult pays no Marketplace premium and is not eligible + # Guam has no Exchange (26 U.S.C. 36B(c)(2)(A); 42 U.S.C. 18024(d)). Without + # the Guam comparison in is_aca_marketplace_enrollee, the premium would be + # charged: + # the infinite fraction is outside the coverage gap. + period: 2026 + input: + people: + person1: + age: 30 + is_medicaid_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_chip_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 30_000 + households: + household: + members: [person1] + state_code: GU + output: + is_aca_marketplace_enrollee: false + pays_aca_premium: false + is_aca_ptc_eligible: false + +- name: 2026 Virgin Islands single adult pays no Marketplace premium and is not eligible + # The Virgin Islands have no Exchange (26 U.S.C. 36B(c)(2)(A); 42 U.S.C. + # 18024(d)). Without the Virgin Islands comparison in + # is_aca_marketplace_enrollee, the premium would be charged: the infinite fraction is outside the coverage + # gap. + period: 2026 + input: + people: + person1: + age: 30 + is_medicaid_eligible: false + is_aca_eshi_eligible: false + is_medicare_eligible: false + is_chip_eligible: false + tax_units: + tax_unit: + members: [person1] + aca_magi: 30_000 + households: + household: + members: [person1] + state_code: VI + output: + is_aca_marketplace_enrollee: false + pays_aca_premium: false + is_aca_ptc_eligible: false diff --git a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py index e20b62b7f6c..dbb1b89095c 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_fpg.py @@ -11,16 +11,18 @@ class aca_fpg(Variable): 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). + # Line 1 (tax family size) and line 4 (poverty line for the state of + # residence). "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#h", ) documentation = ( "Form 8962 line 4: the prior year's poverty guideline for the tax " - "year's tax family size on line 1 (aca_tax_family_size). The former " - "lookup, tax_unit_fpg for the prior year, also took the household " - "size from the prior year; the size now comes from the tax year, as " - "line 1 requires, which differs only when a prior-year size is " - "supplied. A return on which every head and spouse can be claimed " + "year's tax family size on line 1 (aca_tax_family_size) and the tax " + "year's state of residence. The former lookup, tax_unit_fpg for the " + "prior year, also took the household size and state from the prior " + "year; both now come from the tax year, as lines 1 and 4 require. " + "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 " @@ -31,15 +33,20 @@ class aca_fpg(Variable): 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 (26 U.S.C. 36B(d)(3)(B)), and - # the state group is read for the guideline year. For the first - # modeled year, the year before precedes the model, so the tax year's - # guideline stands in and nothing is computed for an unmodeled year. - # For that one year the line uses a later guideline than - # 36B(d)(3)(B) calls for. + # The guideline year lags the tax year (26 U.S.C. 36B(d)(3)(B)). For + # the first modeled year, the year before precedes the model, so the + # tax year's guideline stands in and nothing is computed for an + # unmodeled year. For that one year the line uses a later guideline + # than 36B(d)(3)(B) calls for. guideline_period = period.last_year if guideline_period.start.year < FIRST_MODELED_YEAR: guideline_period = period - state_group = tax_unit.household("state_group_str", guideline_period) - year_lag = period.start.year - guideline_period.start.year - return fpg(size, state_group, period, parameters, year_lag=year_lag) + # Only the guideline lags. The state is the tax year's: line 4 uses + # the guideline for "your state of residence" in the tax year, so + # Alaska and Hawaii households use their own guidelines. 26 CFR + # 1.36B-1(h) applies the higher guideline when the residence moves + # into or out of Alaska or Hawaii during the year, or spouses live in + # different states; the model has one household state a year and + # uses the first member's household. + state_group = tax_unit.household("state_group_str", period) + return fpg(size, state_group, guideline_period, parameters) 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 d3780926dfe..5d965ba851b 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py @@ -7,17 +7,32 @@ class aca_magi_fraction(Variable): label = "ACA-related modified AGI as fraction of prior-year FPL" documentation = ( "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." + "line for the premium tax credit tax family and the state of " + "residence in the coverage year (Form 8962 line 5), truncated to " + "whole percentage points as Worksheet 2 instructs." ) reference = ( # PDF pages 8-9: line 4 (prior-year poverty line for the line 1 - # family size) and line 5 Worksheet 2 (truncation). + # family size and the state of residence) and line 5 Worksheet 2 + # (truncation). "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=8", + "https://www.law.cornell.edu/cfr/text/26/1.36B-1#h", ) definition_period = YEAR def formula(tax_unit, period, parameters): magi = max_(0, tax_unit("aca_magi", period)) - fpg = tax_unit("aca_fpg", period) - return np.floor(100 * magi / fpg) / 100 + guideline = tax_unit("aca_fpg", period) + # HHS defines no guideline for Puerto Rico, Guam or the Virgin + # Islands, whose placeholder is 0, and no Exchange operates there + # (26 U.S.C. 36B(c)(2)(A); 42 U.S.C. 18024(d)). Their fraction is + # infinite rather than 0 / 0 (NaN at $0 of income): the eligibility + # scale's out-of-range cell is false, and the required contribution + # percentage is capped out to 0. is_aca_marketplace_enrollee excludes + # them by state group. + has_guideline = guideline > 0 + # Multiply before dividing, so income exactly on a whole percentage + # (100 x 36,621 / 15,650 = 234) isn't floored one point low by a + # quotient that isn't exactly representable. + percent = 100 * magi / where(has_guideline, guideline, 1) + return where(has_guideline, np.floor(percent) / 100, np.inf) 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 index 46d74a61207..7ed58ff3932 100644 --- a/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py +++ b/policyengine_us/variables/gov/aca/eligibility/is_aca_marketplace_enrollee.py @@ -9,12 +9,21 @@ class is_aca_marketplace_enrollee(Variable): reference = ( "https://www.law.cornell.edu/cfr/text/45/155.305#a", "https://www.law.cornell.edu/uscode/text/26/36B#c_1", + # The credit's plans are those enrolled in through an Exchange + # established by a State (36B(c)(2)(A)); "State" means the 50 States + # and the District of Columbia (42 U.S.C. 18024(d)). + "https://www.law.cornell.edu/uscode/text/26/36B#c_2_A", + "https://www.law.cornell.edu/uscode/text/42/18024#d", ) 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 " + "condition, outside Puerto Rico, Guam and the Virgin Islands, which " + "have no Exchange. A supplied pays_aca_premium of true still marks an " + "enrollee there, as it does for every other condition; the credit " + "stays 0, because the territories' infinite income fraction fails " + "the income test. 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. So a supplied " "pays_aca_premium of true also marks an enrollee." @@ -54,7 +63,22 @@ def formula(person, period, parameters): & ~below_fpl_exception ) - enrolled = is_status_eligible & is_coverage_eligible & ~in_coverage_gap + # Puerto Rico, Guam and the Virgin Islands have no Exchange: 36B(c)(2)(A) + # counts plans enrolled in through an Exchange established by a State, + # and 42 U.S.C. 18024(d) defines a State as the 50 States and the + # District of Columbia. So nobody in them enrolls in a Marketplace + # plan. + state_group = person.household("state_group", period) + groups = state_group.possible_values + has_exchange = ( + (state_group != groups.GU) + & (state_group != groups.PR) + & (state_group != groups.VI) + ) + + enrolled = ( + is_status_eligible & is_coverage_eligible & ~in_coverage_gap & has_exchange + ) # Everyone who pays an age-rated premium is an enrollee, so a supplied # pays_aca_premium of true marks one. The formula of pays_aca_premium # reads this variable, so only a supplied value is read.