From 6c5e6a1bba048ac9700a8f44c166f1fdcb1c4c0d Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Mon, 9 Nov 2026 16:49:14 -0500 Subject: [PATCH 1/4] Count the premium tax credit in household net income as a refundable credit The federal premium tax credit counted in household net income only when gov.simulation.include_health_benefits_in_net_income was on, which it is not by default. A reform to the credit then moved neither net income nor policyengine.py's budgetary impact. household_refundable_tax_credits now adds premium_tax_credit_in_force: the assigned credit, a refundable credit under 26 U.S.C. 36B, zeroed like the other federal refundable credits in a year the federal income tax is abolished. It doesn't go in income_tax, which stays what the return shows (most of the credit is paid in advance to the insurer) and what SPM resources net; SPM resources already count the credit through the marketplace premium net of it. The credit leaves the health-benefits list, and household_net_income_including_health_benefits subtracts it, so it is never counted twice. Tests whose households have a credit now count it: each such case names the credit as an output. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...tc-refundable-credit-net-income.changed.md | 1 + .../household/household_health_benefits.yaml | 3 +- .../military_retirement_pay.yaml | 12 ++++- .../gov/local/tax/local_tax_rollup.yaml | 7 ++- .../gov/states/mt/tax/income/integration.yaml | 25 +++++++--- .../household/household_health_benefits.yaml | 14 +++--- ..._net_income_including_health_benefits.yaml | 9 ++++ .../household/household_net_income.yaml | 15 ++++++ .../household_refundable_tax_credits.yaml | 50 +++++++++++++++++++ .../baseline/household/marginal_tax_rate.yaml | 5 +- .../contrib/federal/abolish_payroll_tax.yaml | 7 ++- .../tests/test_aggregate_list_integrity.py | 1 + .../test_household_spm_resource_parity.py | 7 ++- ...ld_net_income_including_health_benefits.py | 3 ++ .../household_refundable_tax_credits.py | 5 ++ .../tax_unit/premium_tax_credit_in_force.py | 18 +++++++ 16 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 changelog.d/ptc-refundable-credit-net-income.changed.md create mode 100644 policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml create mode 100644 policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py diff --git a/changelog.d/ptc-refundable-credit-net-income.changed.md b/changelog.d/ptc-refundable-credit-net-income.changed.md new file mode 100644 index 00000000000..1932be01e5a --- /dev/null +++ b/changelog.d/ptc-refundable-credit-net-income.changed.md @@ -0,0 +1 @@ +Count the federal premium tax credit in household net income and household tax as a refundable tax credit (household_refundable_tax_credits), whether or not health benefits count in net income, so changes to it reach net income and the budget. income_tax and SPM resources are unchanged. diff --git a/policyengine_us/parameters/gov/household/household_health_benefits.yaml b/policyengine_us/parameters/gov/household/household_health_benefits.yaml index ba051538c04..6d9c4a2c7f4 100644 --- a/policyengine_us/parameters/gov/household/household_health_benefits.yaml +++ b/policyengine_us/parameters/gov/household/household_health_benefits.yaml @@ -4,7 +4,8 @@ values: - medicaid_cost - msp_cost - chip - - assigned_aca_ptc + # The federal premium tax credit counts as a refundable tax credit + # (household_refundable_tax_credits) whether or not health benefits do. - assigned_ca_premium_subsidy - basic_health_program - co_omnisalud diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/military_retirement_pay.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/military_retirement_pay.yaml index befdc3853b7..e8f58b1a6cc 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/military_retirement_pay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/military_retirement_pay.yaml @@ -7,6 +7,7 @@ - name: Case 1, Military retirement pay flows into federal gross income, AGI, and taxable income. period: 2024 + absolute_error_margin: 0.01 input: people: person1: @@ -28,7 +29,10 @@ household_market_income: 30_000 snap_unearned_income: 30_000 ssi_unearned_income: 30_000 - household_net_income: 28_384 + # With no other coverage, a 2024 marketplace credit under the enhanced + # schedule, which household net income counts as a refundable credit. + premium_tax_credit_in_force: 10_323.72 + household_net_income: 28_384 + 10_323.72 - name: Case 2, Military retirement pay survivors flows into federal gross income, AGI, and taxable income. period: 2024 @@ -56,6 +60,7 @@ - name: Case 3, Military retirement pay adds with employment income. period: 2024 + absolute_error_margin: 0.01 input: people: person1: @@ -77,7 +82,10 @@ taxable_income: 70_000 - 14_600 market_income: 70_000 household_market_income: 70_000 - household_net_income: 59_699 + # The enhanced schedule's credit above 400% FPL: the benchmark premium + # minus 8.5% of income. + premium_tax_credit_in_force: 2_846.77 + household_net_income: 59_699 + 2_846.77 - name: Case 4, Dependent military retirement pay is excluded from the tax unit filer gross income. period: 2024 diff --git a/policyengine_us/tests/policy/baseline/gov/local/tax/local_tax_rollup.yaml b/policyengine_us/tests/policy/baseline/gov/local/tax/local_tax_rollup.yaml index c3baf64f188..271e8be6dac 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/tax/local_tax_rollup.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/tax/local_tax_rollup.yaml @@ -91,6 +91,7 @@ - name: Indiana county tax flows from earnings to household and SPM totals period: 2021 + absolute_error_margin: 0.01 input: state_code: IN county_str: BOONE_COUNTY_IN @@ -101,8 +102,10 @@ local_income_tax: 735 local_income_tax_before_refundable_credits: 735 local_tax: 735 - household_tax: 9_037.7 - household_net_income: 40_962.3 + # The 2021 premium tax credit lowers household tax, not the SPM's taxes. + premium_tax_credit_in_force: 1_275.24 + household_tax: 9_037.7 - 1_275.24 + household_net_income: 40_962.3 + 1_275.24 spm_unit_local_tax: 735 spm_unit_taxes: 9_037.7 spm_unit_net_income: 36_824.8 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml index 437b949c97f..e11bb67c787 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml @@ -698,14 +698,18 @@ # 2,339.51 - 1,250 = 1,089.51. state_income_tax: 1_089.51 household_refundable_state_tax_credits: 1_250 - # Federal 2021 recovery rebate credit 1,400 + MT rebate 1,250 = 2,650. - household_refundable_tax_credits: 2_650 + # With no other coverage, a 2021 marketplace credit under the enhanced + # schedule. + premium_tax_credit_in_force: 2_272.79 + # Federal 2021 recovery rebate credit 1,400 + MT rebate 1,250 + premium + # tax credit 2,272.79 = 4,922.79. + household_refundable_tax_credits: 4_922.79 # Federal income tax 4,295 (TI = 50,000 - 12,550 = 37,450; # 10% x 9,950 + 12% x 27,500 = 995 + 3,300) + employee payroll tax # 3,825 (7.65% x 50,000) + MT line 20 2,339.51 = 10,459.51. household_tax_before_refundable_credits: 10_459.51 - # 50,000 + 2,650 - 10,459.51 = 42,190.49 (40,940.49 without the rebate). - household_net_income: 42_190.49 + # 50,000 + 4,922.79 - 10,459.51 = 44,463.28 (43,213.28 without the rebate). + household_net_income: 44_463.28 # SPM resources rise by the same 1,250 through spm_unit_state_tax: # 36,802.99 without the rebate + 1,250 = 38,052.99. spm_unit_net_income: 38_052.99 @@ -744,14 +748,19 @@ # 2,518.22 - 2,500 = 18.22. state_income_tax: 18.22 household_refundable_state_tax_credits: 2_500 - # Federal 2021 recovery rebate credit 2 x 1,400 + MT rebate 2,500 = 5,300. - household_refundable_tax_credits: 5_300 + # With no other coverage, a 2021 marketplace credit for both under the + # enhanced schedule. + premium_tax_credit_in_force: 8_500.58 + # Federal 2021 recovery rebate credit 2 x 1,400 + MT rebate 2,500 + + # premium tax credit 8,500.58 = 13,800.58. + household_refundable_tax_credits: 13_800.58 # Federal income tax 3,790 (TI = 60,000 - 25,100 = 34,900; # 10% x 19,900 + 12% x 15,000 = 1,990 + 1,800) + employee payroll tax # 4,590 (7.65% x 60,000) + MT line 20 2,518.22 = 10,898.22. household_tax_before_refundable_credits: 10_898.22 - # 60,000 + 5,300 - 10,898.22 = 54,401.78 (51,901.78 without the rebate). - household_net_income: 54_401.78 + # 60,000 + 13,800.58 - 10,898.22 = 62,902.36 (60,402.36 without the + # rebate). + household_net_income: 62_902.36 # SPM resources rise by the same 2,500 through spm_unit_state_tax: # 47,581.78 without the rebate + 2,500 = 50,081.78. spm_unit_net_income: 50_081.78 diff --git a/policyengine_us/tests/policy/baseline/household/household_health_benefits.yaml b/policyengine_us/tests/policy/baseline/household/household_health_benefits.yaml index c02d64382ee..27e54bfb1c6 100644 --- a/policyengine_us/tests/policy/baseline/household/household_health_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/household_health_benefits.yaml @@ -12,7 +12,7 @@ household_health_benefits: 0 household_benefits: 0 -- name: Case 2, flag on includes annual canonical ACA PTC and Medicaid value. +- name: Case 2, flag on includes the Medicaid value; the ACA PTC counts as a refundable tax credit instead. period: 2026 absolute_error_margin: 0.01 input: @@ -21,8 +21,8 @@ medicaid_cost: 500 snap: 0 output: - household_health_benefits: 1_500 - household_benefits: 1_500 + household_health_benefits: 500 + household_benefits: 500 - name: Case 3, flag on includes annual MSP and CHIP values. period: 2026 @@ -35,8 +35,8 @@ chip: 300 snap: 0 output: - household_health_benefits: 4_234.8 - household_benefits: 4_234.8 + household_health_benefits: 3_234.8 + household_benefits: 3_234.8 - name: Case 4, legacy wrappers do not double count canonical health values. period: 2026 @@ -52,8 +52,8 @@ msp_cost: 2_434.8 snap: 0 output: - household_health_benefits: 4_234.8 - household_benefits: 4_234.8 + household_health_benefits: 3_234.8 + household_benefits: 3_234.8 - name: Case 5, flag on includes forced Maryland Premium Assistance value. # md_premium_assistance is registered in the household_health_benefits list diff --git a/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml b/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml index 3106c50c673..119d9142b12 100644 --- a/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml @@ -21,3 +21,12 @@ healthcare_benefit_value: 2_000 output: household_net_income_including_health_benefits: 4_000 + +- name: The premium tax credit, in both net income and health benefits, counts once + period: 2026 + input: + household_net_income: 3_000 + healthcare_benefit_value: 1_500 + assigned_aca_ptc: 1_000 + output: + household_net_income_including_health_benefits: 3_000 + 1_500 - 1_000 diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml index ea555b85a89..b9a14416ecb 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml @@ -184,3 +184,18 @@ output: spm_unit_net_income: 500_000 + 1_000 + 200 household_net_income: 500_000 + 1_000 + 200 + +- name: Household net income counts the premium tax credit as a refundable tax credit. + period: 2026 + input: + household_market_income: 30_000 + household_benefits: 0 + income_tax_refundable_credits_in_force: 0 + household_refundable_state_tax_credits: 0 + household_refundable_local_tax_credits: 0 + assigned_aca_ptc: 4_000 + household_tax_before_refundable_credits: 2_000 + household_health_costs: 0 + output: + household_tax: 2_000 - 4_000 + household_net_income: 30_000 + 4_000 - 2_000 diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml new file mode 100644 index 00000000000..f7c4bd246ed --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml @@ -0,0 +1,50 @@ +- name: Case 1, the premium tax credit counts as a refundable tax credit. + period: 2026 + absolute_error_margin: 0.01 + input: + assigned_aca_ptc: 1_000 + income_tax_refundable_credits_in_force: 0 + household_refundable_state_tax_credits: 0 + household_refundable_local_tax_credits: 0 + output: + household_refundable_tax_credits: 1_000 + +- name: Case 2, it counts once whether or not health benefits count in net income. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.simulation.include_health_benefits_in_net_income: true + assigned_aca_ptc: 1_000 + medicaid_cost: 500 + snap: 0 + income_tax_refundable_credits_in_force: 0 + household_refundable_state_tax_credits: 0 + household_refundable_local_tax_credits: 0 + output: + household_refundable_tax_credits: 1_000 + # The health benefits are Medicaid alone. + household_health_benefits: 500 + +- name: Case 3, a marketplace enrollee's credit reaches the household's refundable credits. + # A single 45-year-old in Lebanon County, PA, earning $40,000 (256% of the + # 2025 poverty guideline, which 2026 credits use). The credit is the + # benchmark premium minus the required contribution; at this income there + # is no EITC, refundable CTC or Pennsylvania refundable credit. + period: 2026 + absolute_error_margin: 1 + input: + people: + person1: + age: 45 + employment_income: 40_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: PA + county: LEBANON_COUNTY_PA + output: + assigned_aca_ptc: 10_086 + household_refundable_tax_credits: 10_086 diff --git a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml index a3bbd71491e..a3abeab4ad0 100644 --- a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml +++ b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml @@ -131,7 +131,10 @@ members: [person1, person2, person3] state_code: TX output: - marginal_tax_rate: [0.2965, 0.1965, 0] + # The $60,000 earner (411% FPL) also gets a 2024 premium tax credit under + # the enhanced schedule, which falls by 8.5% of each added dollar: + # 12% + 7.65% + 8.5% = 28.15%. At $90,000 the credit is zero. + marginal_tax_rate: [0.2965, 0.2815, 0] marginal_tax_rate_computed: [true, true, false] federal_marginal_tax_rate: [0.22, 0.12, 0] fica_marginal_tax_rate: [0.0765, 0.0765, 0] diff --git a/policyengine_us/tests/policy/contrib/federal/abolish_payroll_tax.yaml b/policyengine_us/tests/policy/contrib/federal/abolish_payroll_tax.yaml index bee6ad3084c..cc9a8d2a06a 100644 --- a/policyengine_us/tests/policy/contrib/federal/abolish_payroll_tax.yaml +++ b/policyengine_us/tests/policy/contrib/federal/abolish_payroll_tax.yaml @@ -30,6 +30,7 @@ - name: Abolishing payroll tax keeps the Idaho permanent building fund tax period: 2025 + absolute_error_margin: 0.01 input: gov.contrib.ubi_center.flat_tax.abolish_payroll_tax: true state_code: ID @@ -45,8 +46,10 @@ id_pbf: 10 household_state_tax_before_refundable_credits: 2_010 household_tax_before_refundable_credits: 7_010 # 5,000 + 2,000 + 10 - household_refundable_tax_credits: 900 - household_net_income: 53_890 # 60,000 + 900 - 7,010 + # The federal income tax stays, and so does the premium tax credit. + premium_tax_credit_in_force: 122.23 + household_refundable_tax_credits: 800 + 100 + 122.23 + household_net_income: 53_890 + 122.23 # 60,000 + 900 + 122.23 - 7,010 - name: Abolishing payroll tax keeps North Carolina use tax period: 2025 diff --git a/policyengine_us/tests/test_aggregate_list_integrity.py b/policyengine_us/tests/test_aggregate_list_integrity.py index 2599fa64000..747de736ff7 100644 --- a/policyengine_us/tests/test_aggregate_list_integrity.py +++ b/policyengine_us/tests/test_aggregate_list_integrity.py @@ -326,6 +326,7 @@ def test_reform_aggregates_keep_shared_cash_programs( "local_occupational_tax", "household_refundable_tax_credits", "income_tax_refundable_credits_in_force", + "premium_tax_credit_in_force", "federal_income_tax_in_force", "household_refundable_state_tax_credits", "household_refundable_local_tax_credits", diff --git a/policyengine_us/tests/test_household_spm_resource_parity.py b/policyengine_us/tests/test_household_spm_resource_parity.py index b3d554e469a..84eb058ad20 100644 --- a/policyengine_us/tests/test_household_spm_resource_parity.py +++ b/policyengine_us/tests/test_household_spm_resource_parity.py @@ -449,8 +449,10 @@ def test_net_incomes_reconcile_for_one_person(state, earnings): spm_unit_net_income subtracts work, medical and child support costs, and it leaves out health benefits. household_net_income subtracts health costs and - counts the benefits and bill discounts listed in HOUSEHOLD_ONLY. Take those - away and the two totals agree. + counts the benefits and bill discounts listed in HOUSEHOLD_ONLY, and the + premium tax credit as a refundable tax credit; the SPM counts the credit + through lower medical out-of-pocket spending, which spm_side adds back. + Take those away and the two totals agree. """ year = 2026 # Rent makes New Jersey's relief positive, so that payment is under test too. @@ -470,6 +472,7 @@ def value(name): - value("commodity_supplemental_food_program") - value("household_head_start_benefits") - value("housing_assistance") + - value("premium_tax_credit_in_force") ) spm_side = ( value("spm_unit_net_income") diff --git a/policyengine_us/variables/household/household_net_income_including_health_benefits.py b/policyengine_us/variables/household/household_net_income_including_health_benefits.py index 4f4ab385316..2c6a3044e46 100644 --- a/policyengine_us/variables/household/household_net_income_including_health_benefits.py +++ b/policyengine_us/variables/household/household_net_income_including_health_benefits.py @@ -8,3 +8,6 @@ class household_net_income_including_health_benefits(Variable): definition_period = YEAR unit = USD adds = ["household_net_income", "healthcare_benefit_value"] + # household_net_income already counts the premium tax credit, as a + # refundable tax credit, and healthcare_benefit_value counts it too. + subtracts = ["premium_tax_credit_in_force"] diff --git a/policyengine_us/variables/household/income/household/household_refundable_tax_credits.py b/policyengine_us/variables/household/income/household/household_refundable_tax_credits.py index 0a0911e4f8e..b7587568790 100644 --- a/policyengine_us/variables/household/income/household/household_refundable_tax_credits.py +++ b/policyengine_us/variables/household/income/household/household_refundable_tax_credits.py @@ -9,6 +9,11 @@ class household_refundable_tax_credits(Variable): unit = USD adds = [ "income_tax_refundable_credits_in_force", + # The premium tax credit is a refundable credit (26 U.S.C. 36B), but + # mostly paid in advance to the insurer, with the return reconciling + # the difference. It counts here rather than in income_tax, which + # stays what the return shows and what SPM resources net. + "premium_tax_credit_in_force", "household_refundable_state_tax_credits", "household_refundable_local_tax_credits", ] diff --git a/policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py b/policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py new file mode 100644 index 00000000000..98598ab03e7 --- /dev/null +++ b/policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class premium_tax_credit_in_force(Variable): + value_type = float + entity = TaxUnit + label = "premium tax credit counted in household net income" + documentation = ( + "The assigned premium tax credit, a refundable federal income tax " + "credit (26 U.S.C. 36B), as household net income counts it. Like the " + "other federal refundable credits, it is zero in a year the federal " + "income tax is abolished." + ) + definition_period = YEAR + unit = USD + reference = "https://www.law.cornell.edu/uscode/text/26/36B" + defined_for = "federal_income_tax_in_force" + adds = ["assigned_aca_ptc"] From 38ca45274f7529cb089e872e18866aa8e20fbf3a Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Mon, 9 Nov 2026 17:57:25 -0500 Subject: [PATCH 2/4] Keep the roommate and zero-rate tests off the credit; pin its marginal rate The roommate marginal tax rate tests and the zero-rate capital gains response test check mechanics the premium tax credit's phase-out would blur, so their households now have employer coverage. A separate case pins the phase-out: a single $60,000 earner in 2024 faces 12% + 7.65% + 8.5% = 28.15%. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../test_behavioral_response_measurements.py | 11 +++++-- .../core/test_marginal_tax_rate_coverage.py | 7 +++- .../baseline/household/marginal_tax_rate.yaml | 32 ++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/policyengine_us/tests/core/test_behavioral_response_measurements.py b/policyengine_us/tests/core/test_behavioral_response_measurements.py index 7ef5166ee81..55814bca7fc 100644 --- a/policyengine_us/tests/core/test_behavioral_response_measurements.py +++ b/policyengine_us/tests/core/test_behavioral_response_measurements.py @@ -649,7 +649,8 @@ def test_semi_elasticity_capital_gains_response_end_to_end_from_a_zero_rate(): """The semi-elasticity form through the model, from a 0% baseline rate. A Texas filer whose $30,000 of gains sit in the 0% bracket faces a new 10% - rate there. The semi-elasticity response depends only on the change in the + rate there. The filer has employer coverage, so no premium tax credit + phases out with the gains. The semi-elasticity response depends only on the change in the measured rate, so it is about 29% at 3.4, where the log-rate form's 0.001 floor would remove about 96% (test_capital_gains_response_forms.py covers the forms' arithmetic without a simulation). @@ -659,7 +660,13 @@ def test_semi_elasticity_capital_gains_response_end_to_end_from_a_zero_rate(): year = "2026" gains = 30_000 situation = { - "people": {"p": {"age": {year: 50}, "long_term_capital_gains": {year: gains}}}, + "people": { + "p": { + "age": {year: 50}, + "long_term_capital_gains": {year: gains}, + "has_esi": {year: True}, + } + }, "tax_units": {"tu": {"members": ["p"]}}, "families": {"f": {"members": ["p"]}}, "spm_units": {"s": {"members": ["p"]}}, diff --git a/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py index a15eb5199b7..da71909814a 100644 --- a/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py +++ b/policyengine_us/tests/core/test_marginal_tax_rate_coverage.py @@ -179,6 +179,7 @@ def test_raising_the_cap_only_adds_people(mtr_by_cap, variable): # wages less the $14,600 standard deduction ($75,400, $45,400, $15,400), so a # $1,000 raise is taxed at 22%, 12% and 12%, plus 7.65% employee payroll tax. # Nobody crosses a bracket or payroll threshold, and Texas has no income tax. +# Each has employer coverage, so no premium tax credit phases out with a raise. ROOMMATE_WAGES = (90_000, 60_000, 30_000) ROOMMATE_RATES = { "marginal_tax_rate": (0.2965, 0.1965, 0.1965), @@ -193,7 +194,11 @@ def test_roommates_match_hand_computed_rates(cap): names = [f"roommate{i}" for i in range(len(ROOMMATE_WAGES))] situation = { "people": { - name: {"age": {YEAR: 40 - 5 * i}, "employment_income": {YEAR: wages}} + name: { + "age": {YEAR: 40 - 5 * i}, + "employment_income": {YEAR: wages}, + "has_esi": {YEAR: True}, + } for i, (name, wages) in enumerate(zip(names, ROOMMATE_WAGES)) }, "tax_units": {f"{name}_tax_unit": {"members": [name]} for name in names}, diff --git a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml index a3abeab4ad0..bd23acb269c 100644 --- a/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml +++ b/policyengine_us/tests/policy/baseline/household/marginal_tax_rate.yaml @@ -103,15 +103,20 @@ period: 2024 input: people: + # Each has employer coverage, so no premium tax credit phases out + # with a raise. person1: age: 40 employment_income: 90_000 + has_esi: true person2: age: 35 employment_income: 60_000 + has_esi: true person3: age: 30 employment_income: 30_000 + has_esi: true tax_units: tax_unit1: members: [person1] @@ -131,14 +136,33 @@ members: [person1, person2, person3] state_code: TX output: - # The $60,000 earner (411% FPL) also gets a 2024 premium tax credit under - # the enhanced schedule, which falls by 8.5% of each added dollar: - # 12% + 7.65% + 8.5% = 28.15%. At $90,000 the credit is zero. - marginal_tax_rate: [0.2965, 0.2815, 0] + marginal_tax_rate: [0.2965, 0.1965, 0] marginal_tax_rate_computed: [true, true, false] federal_marginal_tax_rate: [0.22, 0.12, 0] fica_marginal_tax_rate: [0.0765, 0.0765, 0] +- name: A marketplace enrollee's rate includes the premium tax credit's phase-out + # A single 35-year-old in Texas earning $60,000 in 2024: 411% of the 2023 + # poverty guideline, where the enhanced schedule caps the benchmark plan at + # 8.5% of income, so the credit falls by 8.5 cents per added dollar. + # 12% bracket + 7.65% payroll tax + 8.5% = 28.15%. + absolute_error_margin: 0.0001 + period: 2024 + input: + people: + person1: + age: 35 + employment_income: 60_000 + tax_units: + tax_unit1: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + marginal_tax_rate: 0.2815 + # A single Ohio parent of children aged 4 and 8. The marginal tax rate must # equal the change in household net income between the two fresh simulations # above it: 1 - (alt - base) / 1_000. Ohio Works First counts From 460f68252e95b38474e3195bd00689b60d370b3c Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Mon, 9 Nov 2026 21:37:01 -0500 Subject: [PATCH 3/4] Keep 2015 credits within the model; keep two more tests off the credit Household net income now computes the premium tax credit for every household, and the credit's FPL fraction read the prior year's poverty guideline, so a 2015 household computed 2014 values, before the first modeled year (test_irmaa_first_modeled_year). For coverage in the first modeled year, the coverage year's guideline now stands in, as Colorado CCAP reads its parameters at the start of that year. The estate income invariant and the Social Security COLA differential check net income against tax-only arithmetic and main's literals, so their filers have employer coverage. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/core/test_estate_income_invariants.py | 4 +++- policyengine_us/tests/unit/test_ss_cola_means_test.py | 3 +++ .../variables/gov/aca/eligibility/aca_magi_fraction.py | 10 +++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/core/test_estate_income_invariants.py b/policyengine_us/tests/core/test_estate_income_invariants.py index 4e458c5a79d..271584b08d0 100644 --- a/policyengine_us/tests/core/test_estate_income_invariants.py +++ b/policyengine_us/tests/core/test_estate_income_invariants.py @@ -480,7 +480,8 @@ def test_estate_income_raises_household_net_income_by_its_after_tax_amount( A single Texas filer with $50,000 of wages in 2024 receives $10,000 of estate income, which stays in the 12% bracket. Texas has no income tax and estate income carries no payroll tax, so household net income rises - by the $10,000 less the federal income tax increase. + by the $10,000 less the federal income tax increase. The filer has + employer coverage, so no premium tax credit falls as income rises. """ def net_income(estate): @@ -488,6 +489,7 @@ def net_income(estate): "age": {2024: 40}, "employment_income": {2024: 50_000}, "estate_income": {2024: estate}, + "has_esi": {2024: True}, } if qualified is not None: head["estate_income_would_be_qualified"] = {2024: qualified} diff --git a/policyengine_us/tests/unit/test_ss_cola_means_test.py b/policyengine_us/tests/unit/test_ss_cola_means_test.py index e43f3dafbbf..e07b326ae37 100644 --- a/policyengine_us/tests/unit/test_ss_cola_means_test.py +++ b/policyengine_us/tests/unit/test_ss_cola_means_test.py @@ -1151,9 +1151,12 @@ def test_means_test_off_preserves_downstream_dependency_closure( for component in COMPONENTS: record[f"social_security_{component}"] = {2027: 6_000} record.pop(f"social_security_{component}_reported") + # Employer coverage keeps the worker off the premium tax credit, which + # household net income counts since main supplied these literals. record.update( employment_income={2027: 10_000}, taxable_interest_income={2027: 20_000}, + has_esi={2027: True}, ) situation = _situation({"worker": record}) situation["households"]["worker"]["state_code"] = {2027: "NY"} 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..f361e2e80c5 100644 --- a/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py +++ b/policyengine_us/variables/gov/aca/eligibility/aca_magi_fraction.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.parameters import FIRST_MODELED_YEAR class aca_magi_fraction(Variable): @@ -24,5 +25,12 @@ class aca_magi_fraction(Variable): def formula(tax_unit, period, parameters): magi = max_(0, tax_unit("aca_magi", period)) - fpg = tax_unit("tax_unit_fpg", period.last_year) + # The credit measures income against the guideline for the year before + # the coverage year. For coverage in the first modeled year, that year + # precedes the model, so the coverage year's guideline stands in and + # nothing is computed for an unmodeled year. + guideline_period = period.last_year + if guideline_period.start.year < FIRST_MODELED_YEAR: + guideline_period = period + fpg = tax_unit("tax_unit_fpg", guideline_period) return np.floor(100 * magi / fpg) / 100 From e0a7f08ca587afb0815fa992e3925330d9bb9126 Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Tue, 10 Nov 2026 10:38:25 -0500 Subject: [PATCH 4/4] Count the used credit once, also with health benefits counted Review on #10773 (Max): - premium_tax_credit_in_force reads used_aca_ptc, which 26 U.S.C. 36B(b)(2) limits to the premiums of the plan the tax unit enrolls in (and which is zero for a tax unit that doesn't enroll), instead of assigned_aca_ptc. - With include_health_benefits_in_net_income on, household_health_costs subtracts the whole Marketplace premium instead of the net premium. Net income counts the credit as a refundable credit, so the net premium counted it a second time: a $94 smaller credit cost $188. Tests: a flag-on pair ($1,000 more credit against a $5,000 premium raises net income $1,000), a credit capped at an $800 plan, and the 2017 cliffs analysis household (a $30,000 to $31,000 raise in Texas in 2025 raises net income $709.50 with or without health benefits counted). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../gov/household/household_health_costs.yaml | 5 +- ..._net_income_including_health_benefits.yaml | 2 +- .../household/household_health_costs.yaml | 17 +++- .../household/household_net_income.yaml | 88 ++++++++++++++++++- .../household_refundable_tax_credits.yaml | 21 ++++- .../tax_unit/premium_tax_credit_in_force.py | 14 +-- 6 files changed, 132 insertions(+), 15 deletions(-) diff --git a/policyengine_us/parameters/gov/household/household_health_costs.yaml b/policyengine_us/parameters/gov/household/household_health_costs.yaml index e19592dc88b..6eb5cd92cbc 100644 --- a/policyengine_us/parameters/gov/household/household_health_costs.yaml +++ b/policyengine_us/parameters/gov/household/household_health_costs.yaml @@ -2,7 +2,10 @@ description: The government counts these sources as household health out-of-pock values: 2022-01-01: - chip_premium - - marketplace_net_premium + # The whole premium: household net income counts the premium tax credit + # that pays part of it as a refundable credit, so the net premium would + # count the credit twice. + - selected_marketplace_plan_premium_proxy metadata: unit: list diff --git a/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml b/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml index 119d9142b12..462e56f3035 100644 --- a/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/household_net_income_including_health_benefits.yaml @@ -27,6 +27,6 @@ input: household_net_income: 3_000 healthcare_benefit_value: 1_500 - assigned_aca_ptc: 1_000 + premium_tax_credit_in_force: 1_000 output: household_net_income_including_health_benefits: 3_000 + 1_500 - 1_000 diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_health_costs.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_health_costs.yaml index 32a7b5f3bcc..7f51040fea0 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_health_costs.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_health_costs.yaml @@ -14,16 +14,27 @@ output: household_health_costs: 360 -- name: Household health costs include Marketplace net premium when health benefits are included in net income. +- name: Household health costs include the whole Marketplace premium when health benefits are included in net income. period: 2024 input: gov.simulation.include_health_benefits_in_net_income: true chip_premium: 360 - marketplace_net_premium: 2_000 + selected_marketplace_plan_premium_proxy: 2_000 output: household_health_costs: 2_360 -- name: Household health costs include computed unsubsidized Marketplace net premium. +- name: The premium tax credit doesn't lower household health costs, since net income counts it as a refundable credit. + period: 2026 + input: + gov.simulation.include_health_benefits_in_net_income: true + chip_premium: 0 + selected_marketplace_plan_premium_proxy: 5_000 + aca_ptc: 1_000 + output: + marketplace_net_premium: 4_000 + household_health_costs: 5_000 + +- name: Household health costs include a computed unsubsidized Marketplace premium. absolute_error_margin: 0.01 period: 2026 input: diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml index b9a14416ecb..611ae4f1c87 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_net_income.yaml @@ -193,9 +193,95 @@ income_tax_refundable_credits_in_force: 0 household_refundable_state_tax_credits: 0 household_refundable_local_tax_credits: 0 - assigned_aca_ptc: 4_000 + used_aca_ptc: 4_000 household_tax_before_refundable_credits: 2_000 household_health_costs: 0 output: household_tax: 2_000 - 4_000 household_net_income: 30_000 + 4_000 - 2_000 + +# With health benefits counted, health costs subtract the whole Marketplace +# premium, so a credit that is $1,000 larger raises net income by $1,000, not +# $2,000 (once as a refundable credit and again through a smaller net premium). +- name: With health benefits counted, a $1,000 premium tax credit against a $5,000 premium. + period: 2026 + input: + gov.simulation.include_health_benefits_in_net_income: true + household_market_income: 30_000 + household_benefits: 0 + income_tax_refundable_credits_in_force: 0 + household_refundable_state_tax_credits: 0 + household_refundable_local_tax_credits: 0 + household_tax_before_refundable_credits: 2_000 + chip_premium: 0 + selected_marketplace_plan_premium_proxy: 5_000 + aca_ptc: 1_000 + output: + premium_tax_credit_in_force: 1_000 + household_health_costs: 5_000 + household_net_income: 30_000 + 1_000 - 2_000 - 5_000 + +- name: With health benefits counted, a $2,000 premium tax credit against the same premium. + period: 2026 + input: + gov.simulation.include_health_benefits_in_net_income: true + household_market_income: 30_000 + household_benefits: 0 + income_tax_refundable_credits_in_force: 0 + household_refundable_state_tax_credits: 0 + household_refundable_local_tax_credits: 0 + household_tax_before_refundable_credits: 2_000 + chip_premium: 0 + selected_marketplace_plan_premium_proxy: 5_000 + aca_ptc: 2_000 + output: + premium_tax_credit_in_force: 2_000 + household_health_costs: 5_000 + household_net_income: 30_000 + 2_000 - 2_000 - 5_000 + +# A single Texas adult aged 40 in 2025 (the 2017 cliffs analysis household, +# PolicyEngine/us-cliffs-2017 scripts/ptc_household.py), with health benefits +# counted. A $1,000 raise cuts the premium tax credit by $94, and net income +# rises by $709.50 with or without health benefits counted (31,840.09 to +# 32,549.59 without): the cut counts once. +- name: With health benefits counted, a Texas adult earning $30,000 in 2025. + period: 2025 + absolute_error_margin: 0.01 + input: + gov.simulation.include_health_benefits_in_net_income: true + people: + person1: + age: 40 + employment_income: 30_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + premium_tax_credit_in_force: 5_606.58 + household_health_costs: 6_194.58 + household_net_income: 25_645.50 + +- name: With health benefits counted, the same adult earning $31,000. + period: 2025 + absolute_error_margin: 0.01 + input: + gov.simulation.include_health_benefits_in_net_income: true + people: + person1: + age: 40 + employment_income: 31_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + premium_tax_credit_in_force: 5_512.58 + household_health_costs: 6_194.58 + household_net_income: 25_645.50 + 709.50 diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml index f7c4bd246ed..434b26458d4 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_refundable_tax_credits.yaml @@ -2,7 +2,7 @@ period: 2026 absolute_error_margin: 0.01 input: - assigned_aca_ptc: 1_000 + used_aca_ptc: 1_000 income_tax_refundable_credits_in_force: 0 household_refundable_state_tax_credits: 0 household_refundable_local_tax_credits: 0 @@ -14,7 +14,7 @@ absolute_error_margin: 0.01 input: gov.simulation.include_health_benefits_in_net_income: true - assigned_aca_ptc: 1_000 + used_aca_ptc: 1_000 medicaid_cost: 500 snap: 0 income_tax_refundable_credits_in_force: 0 @@ -25,7 +25,22 @@ # The health benefits are Medicaid alone. household_health_benefits: 500 -- name: Case 3, a marketplace enrollee's credit reaches the household's refundable credits. +- name: Case 3, the credit counted is limited to the premium of the plan the household enrolls in. + # 26 U.S.C. 36B(b)(2): a $1,000 credit against an $800 plan counts $800. + period: 2026 + absolute_error_margin: 0.01 + input: + aca_ptc: 1_000 + selected_marketplace_plan_premium_proxy: 800 + income_tax_refundable_credits_in_force: 0 + household_refundable_state_tax_credits: 0 + household_refundable_local_tax_credits: 0 + output: + used_aca_ptc: 800 + premium_tax_credit_in_force: 800 + household_refundable_tax_credits: 800 + +- name: Case 4, a marketplace enrollee's credit reaches the household's refundable credits. # A single 45-year-old in Lebanon County, PA, earning $40,000 (256% of the # 2025 poverty guideline, which 2026 credits use). The credit is the # benchmark premium minus the required contribution; at this income there diff --git a/policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py b/policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py index 98598ab03e7..d6860c572ff 100644 --- a/policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py +++ b/policyengine_us/variables/household/income/tax_unit/premium_tax_credit_in_force.py @@ -6,13 +6,15 @@ class premium_tax_credit_in_force(Variable): entity = TaxUnit label = "premium tax credit counted in household net income" documentation = ( - "The assigned premium tax credit, a refundable federal income tax " - "credit (26 U.S.C. 36B), as household net income counts it. Like the " - "other federal refundable credits, it is zero in a year the federal " - "income tax is abolished." + "The premium tax credit, a refundable federal income tax credit " + "(26 U.S.C. 36B), as household net income counts it: the credit the " + "tax unit uses, which 36B(b)(2) limits to the premiums of the plan " + "it enrolls in and which is zero for a tax unit that doesn't enroll. " + "Like the other federal refundable credits, it is zero in a year the " + "federal income tax is abolished." ) definition_period = YEAR unit = USD - reference = "https://www.law.cornell.edu/uscode/text/26/36B" + reference = "https://www.law.cornell.edu/uscode/text/26/36B#b_2" defined_for = "federal_income_tax_in_force" - adds = ["assigned_aca_ptc"] + adds = ["used_aca_ptc"]